打造专业体验:青绿五子棋(三)—— 禁手规则详解与实现(Flutter + OpenHarmony)

个人主页:ujainu

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

文章目录

    • [引言:从娱乐到竞技 ------ 五子棋的专业化之路](#引言:从娱乐到竞技 —— 五子棋的专业化之路)
    • [一、为什么需要禁手?------ 五子棋的竞技进化史](#一、为什么需要禁手?—— 五子棋的竞技进化史)
      • [1. 先手优势的数学证明](#1. 先手优势的数学证明)
      • [2. RIF 规则的核心:禁手体系](#2. RIF 规则的核心:禁手体系)
      • [3. 禁手判负机制](#3. 禁手判负机制)
    • 二、核心技术一:禁手检测算法设计
      • [1. 整体检测流程](#1. 整体检测流程)
      • [2. 长连禁手:最简单的判定](#2. 长连禁手:最简单的判定)
      • [3. 四四禁手:统计"四"的数量](#3. 四四禁手:统计“四”的数量)
      • [4. 三三禁手:识别"活三"](#4. 三三禁手:识别“活三”)
    • 三、核心技术二:禁手综合判定函数
      • [边界处理:_extractLine 函数](#边界处理:_extractLine 函数)
    • [四、核心技术三:UI 反馈与禁手可视化](#四、核心技术三:UI 反馈与禁手可视化)
      • [1. 红色闪烁棋子](#1. 红色闪烁棋子)
      • [2. 禁手提示弹窗](#2. 禁手提示弹窗)
      • [3. 高亮禁手位置](#3. 高亮禁手位置)
    • [五、核心技术四:游戏模式扩展 ------ 禁手开关](#五、核心技术四:游戏模式扩展 —— 禁手开关)
      • [1. 新增设置项](#1. 新增设置项)
      • [2. 状态传递](#2. 状态传递)
      • [3. 落子逻辑整合](#3. 落子逻辑整合)
    • [六、AI 适配:让 AI 遵守禁手规则](#六、AI 适配:让 AI 遵守禁手规则)
      • [1. AI 落子前预检](#1. AI 落子前预检)
    • 七、完整可运行代码
    • 运行界面
    • 结语:专业规则,成就竞技之美

引言:从娱乐到竞技 ------ 五子棋的专业化之路

在前两篇中,我们成功构建了一款兼具东方美学与智能交互的五子棋应用:第一篇实现了双人对战与青绿山水界面,第二篇引入了基于评分策略的 AI 对手。然而,若要真正迈向专业级五子棋体验 ,我们必须面对一个核心命题:禁手规则

五子棋虽看似简单,但其竞技版本(即"连珠")自 20 世纪 90 年代起便采用 RIF(Renju International Federation)规则 ,其中最关键的一条便是:黑棋先行优势过大,故施加禁手限制。这一规则不仅平衡了先手优势,更将五子棋从"谁先连五谁赢"的简单游戏,升华为一门讲究布局、陷阱与反制的艺术。

本文将深入剖析 黑棋三大禁手 (三三、四四、长连)的判定逻辑,并在 Flutter + OpenHarmony 环境中实现一套可开关的专业规则系统。玩家可在游戏开始前选择是否启用禁手,满足从休闲娱乐到竞技训练的多元需求。

我们将重点解决以下技术挑战:

  • 如何精准识别"活三"与"冲四"?
  • 如何判断"同时形成两个以上"有效棋型?
  • 如何在 UI 上给予清晰、震撼的禁手反馈?
  • 如何无缝集成到现有游戏架构中,不破坏原有逻辑?

一、为什么需要禁手?------ 五子棋的竞技进化史

1. 先手优势的数学证明

研究表明,在无禁手规则下,黑棋存在必胜策略。1993 年,Victor Allis 通过计算机证明:在 15×15 棋盘上,黑棋可通过特定开局(如"花月"、"浦月")强制获胜。这意味着,若无限制,白棋从第一步起就处于绝对劣势。

2. RIF 规则的核心:禁手体系

为平衡先手优势,RIF 制定了三大禁手(仅对黑棋生效):

禁手类型 定义 示例
三三禁手 一步棋同时形成 两个或以上活三 .BB.B. + .B.BB. 同时成立
四四禁手 一步棋同时形成 两个或以上四(活四或冲四) WBBBB. + .BBBBW 同时成立
长连禁手 形成 六子或以上连线 BBBBBB

关键说明

  • "活三" = 两端皆空的三连(如 .BBB.
  • "四" = 包括活四(.BBBB.)和冲四(WBBBB..BBBBW
  • 白棋无禁手,可自由形成任意棋型

3. 禁手判负机制

一旦黑棋落子触发任一禁手,立即判白方胜,无需等待五子连珠。这是对"不公平优势"的直接惩罚。


二、核心技术一:禁手检测算法设计

1. 整体检测流程

黑棋落子后,执行以下步骤:

dart 复制代码
if (player == 1 && _forbiddenEnabled) {
  if (checkForbiddenMove(row, col)) {
    _winner = 2; // 白胜
    _forbiddenPosition = Offset(col.toDouble(), row.toDouble());
    showForbiddenDialog();
    return true;
  }
}

2. 长连禁手:最简单的判定

只需检查四个方向是否有 ≥6 连:

dart 复制代码
bool _checkLongLine(int row, int col) {
  for (var dir in directions) {
    int count = 1;
    // 正向
    int r = row + dir[0];
    int c = col + dir[1];
    while (r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE && _board[r][c] == 1) {
      count++;
      r += dir[0];
      c += dir[1];
    }
    // 反向
    r = row - dir[0];
    c = col - dir[1];
    while (r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE && _board[r][c] == 1) {
      count++;
      r -= dir[0];
      c -= dir[1];
    }
    if (count >= 6) return true;
  }
  return false;
}

3. 四四禁手:统计"四"的数量

关键在于:区分活四与冲四,并计数

dart 复制代码
int _countFours(int row, int col) {
  int fourCount = 0;
  for (var dir in directions) {
    final line = _extractLine(row, col, dir, 5); // 提取5格
    if (_isFour(line, 1)) {
      fourCount++;
    }
  }
  return fourCount;
}

bool _isFour(List<int> pattern, int player) {
  final opponent = 3 - player; // 1->2, 2->1
  if (pattern.length != 5) return false;
  final selfCount = pattern.where((p) => p == player).length;
  final emptyCount = pattern.where((p) => p == 0).length;
  final opponentCount = pattern.where((p) => p == opponent).length;
  
  if (selfCount != 4 || opponentCount > 0) return false;
  return emptyCount == 1; // 4子+1空 = 四(活或冲)
}

💡 注意
_extractLine 需处理边界,将越界位置视为对手子(阻挡)

4. 三三禁手:识别"活三"

难点在于:必须是"活三"(两端皆空)

dart 复制代码
int _countLiveThrees(int row, int col) {
  int liveThreeCount = 0;
  for (var dir in directions) {
    // 检查以(row,col)为中心的7格(确保能判断两端)
    final line = _extractLine(row, col, dir, 7);
    if (_isLiveThree(line, 1)) {
      liveThreeCount++;
    }
  }
  return liveThreeCount;
}

bool _isLiveThree(List<int> line, int player) {
  // 在7格中找连续3个player,且两端为空
  for (int i = 0; i <= 4; i++) {
    if (line[i] == 0 && 
        line[i+1] == player && 
        line[i+2] == player && 
        line[i+3] == player && 
        line[i+4] == 0) {
      return true;
    }
  }
  return false;
}

⚠️ 重要优化

使用 7 格窗口而非 5 格,才能准确判断"两端是否为空"


三、核心技术二:禁手综合判定函数

整合三大禁手检测:

dart 复制代码
bool checkForbiddenMove(int row, int col) {
  // 1. 长连禁手
  if (_checkLongLine(row, col)) {
    _forbiddenType = '长连';
    return true;
  }

  // 2. 统计四的数量
  final fourCount = _countFours(row, col);
  if (fourCount >= 2) {
    _forbiddenType = '四四';
    return true;
  }

  // 3. 统计活三的数量
  final liveThreeCount = _countLiveThrees(row, col);
  if (liveThreeCount >= 2) {
    _forbiddenType = '三三';
    return true;
  }

  return false;
}

边界处理:_extractLine 函数

dart 复制代码
List<int> _extractLine(int row, int col, List<int> dir, int length) {
  final half = length ~/ 2;
  final line = <int>[];
  for (int i = -half; i <= half; i++) {
    final r = row + i * dir[0];
    final c = col + i * dir[1];
    if (r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE) {
      line.add(_board[r][c]);
    } else {
      line.add(2); // 边界视为白子(阻挡)
    }
  }
  return line;
}

为何边界视为白子?

因为边界会阻挡棋型延伸,等效于被对手堵住


四、核心技术三:UI 反馈与禁手可视化

1. 红色闪烁棋子

使用 AnimatedContainer 实现颜色脉动:

dart 复制代码
Widget _buildStone(int player, {bool isForbidden = false}) {
  if (isForbidden) {
    return AnimatedContainer(
      duration: const Duration(milliseconds: 800),
      curve: Curves.easeInOut,
      decoration: BoxDecoration(
        color: Colors.red,
        shape: BoxShape.circle,
        boxShadow: [
          BoxShadow(
            color: Colors.red.withOpacity(0.5),
            blurRadius: 12,
            spreadRadius: 2,
          ),
        ],
      ),
      width: _cellSize * 0.8,
      height: _cellSize * 0.8,
    );
  }
  // ... normal stone
}

2. 禁手提示弹窗

dart 复制代码
void _showForbiddenDialog() {
  showDialog(
    context: context,
    builder: (context) => AlertDialog(
      title: const Text('❌ 黑棋禁手!', style: TextStyle(color: Colors.red)),
      content: Text('检测到 $_forbiddenType 禁手,白方获胜!'),
      actions: [
        Center(
          child: ElevatedButton(
            onPressed: () {
              Navigator.of(context).pop();
              _startNewGame();
            },
            child: const Text('重新开始'),
          ),
        ),
      ],
    ),
  );
}

3. 高亮禁手位置

CustomPainter 中绘制红色圆环:

dart 复制代码
// In _GomokuBoardPainter
if (_forbiddenPosition != null) {
  final x = 24.0 + _forbiddenPosition!.dx * cellSize;
  final y = 24.0 + _forbiddenPosition!.dy * cellSize;
  final forbiddenPaint = Paint()
    ..color = Colors.red.withOpacity(0.6)
    ..style = PaintingStyle.stroke
    ..strokeWidth = 4;
  canvas.drawCircle(Offset(x, y), cellSize * 0.5, forbiddenPaint);
}

五、核心技术四:游戏模式扩展 ------ 禁手开关

1. 新增设置项

在模式选择弹窗中加入禁手选项:

dart 复制代码
bool _forbiddenEnabled = false;

// In _showModeSelection
CheckboxListTile(
  title: const Text('启用黑棋禁手(专业规则)'),
  value: _forbiddenEnabled,
  onChanged: (value) {
    setState(() {
      _forbiddenEnabled = value ?? false;
    });
  },
),

2. 状态传递

确保 _forbiddenEnabled 在新游戏时保留:

dart 复制代码
void _startNewGame() {
  // ... reset board
  // 保留 _forbiddenEnabled 和 _gameMode
}

3. 落子逻辑整合

_placeStone 中加入禁手检查:

dart 复制代码
void _placeStone(int row, int col, {bool isAI = false}) {
  if (_winner != null || _board[row][col] != 0) return;

  final player = _currentPlayer;
  setState(() {
    _board[row][col] = player;
    
    // 禁手检查(仅黑棋且启用禁手时)
    if (player == 1 && _forbiddenEnabled) {
      if (checkForbiddenMove(row, col)) {
        _winner = 2;
        _forbiddenPosition = Offset(col.toDouble(), row.toDouble());
        Future.delayed(const Duration(milliseconds: 300), _showForbiddenDialog);
        return;
      }
    }
    
    // 常规胜负检查
    if (checkWin(row, col, player)) {
      _winner = player;
      Future.delayed(const Duration(milliseconds: 500), _showWinDialog);
    } else {
      _currentPlayer = 3 - player; // 切换玩家
      // ... AI logic
    }
  });
}

六、AI 适配:让 AI 遵守禁手规则

1. AI 落子前预检

_makeAIMove 中,跳过会导致禁手的位置:

dart 复制代码
for (int row = 0; row < BOARD_SIZE; row++) {
  for (int col = 0; col < BOARD_SIZE; col++) {
    if (_board[row][col] != 0) continue;
    
    // 如果是黑棋AI,且启用禁手,预检禁手
    if (_aiPlayer == 1 && _forbiddenEnabled) {
      // 临时落子
      _board[row][col] = 1;
      final isForbidden = checkForbiddenMove(row, col);
      _board[row][col] = 0; // 撤销
      if (isForbidden) continue; // 跳过禁手点
    }
    
    // ... 计算得分
  }
}

性能考量

禁手检测本身很快(O(1)),对 AI 性能影响可忽略


七、完整可运行代码

以下为整合禁手规则、模式选择、AI 适配的完整实现:

dart 复制代码
import 'package:flutter/material.dart';
import 'dart:math';

void main() => runApp(const GomokuApp());

class GomokuApp extends StatelessWidget {
  const GomokuApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: '青绿五子棋',
      theme: ThemeData(
        primarySwatch: Colors.teal,
        scaffoldBackgroundColor: const Color(0xFFF5F7FA),
      ),
      home: GomokuGame(),
    );
  }
}

enum GameMode {
  twoPlayer,
  humanVsAIAsBlack,
  humanVsAIAsWhite,
}

class GomokuGame extends StatefulWidget {
  @override
  State<GomokuGame> createState() => _GomokuGameState();
}

class _GomokuGameState extends State<GomokuGame> with TickerProviderStateMixin {
  static const int BOARD_SIZE = 15;
  static const double PADDING = 24.0;
  static const Color BACKGROUND_START = Color(0xFFE6F7F4);
  static const Color BACKGROUND_END = Color(0xFFB8E6D9);
  static const Color LINE_COLOR = Color(0xFF888888);

  late List<List<int>> _board;
  int _currentPlayer = 1;
  int? _winner;
  double _cellSize = 0;
  List<Offset> _winningPath = [];
  GameMode _gameMode = GameMode.twoPlayer;
  int _humanPlayer = 1;
  int _aiPlayer = 2;
  bool _forbiddenEnabled = false;
  Offset? _forbiddenPosition;
  String _forbiddenType = '';

  // 记录最新 AI 落子位置(用于淡入动画)
  Offset? _latestAIMove;

  late AnimationController _aiAnimationController;
  late Animation<double> _aiOpacity;

  final List<List<int>> directions = [
    [0, 1],
    [1, 0],
    [1, 1],
    [1, -1],
  ];

  bool _hasShownInitialDialog = false;

  @override
  void initState() {
    super.initState();
    _aiAnimationController = AnimationController(
      duration: const Duration(milliseconds: 300),
      vsync: this,
    );
    _aiOpacity = Tween<double>(begin: 0, end: 1).animate(_aiAnimationController);

    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (mounted && !_hasShownInitialDialog) {
        _hasShownInitialDialog = true;
        _showModeSelection();
      }
    });
  }

  @override
  void dispose() {
    _aiAnimationController.dispose();
    super.dispose();
  }

  void _showModeSelection() {
    if (!mounted) return;

    showDialog(
      context: context,
      builder: (context) => StatefulBuilder(
        builder: (context, setState) {
          return AlertDialog(
            title: const Text('选择游戏模式'),
            content: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                RadioListTile<GameMode>(
                  title: const Text('👥 双人对战'),
                  value: GameMode.twoPlayer,
                  groupValue: _gameMode,
                  onChanged: (value) {
                    if (value != null) {
                      setState(() {
                        _gameMode = value;
                      });
                    }
                  },
                ),
                RadioListTile<GameMode>(
                  title: const Text('⚫ 人机对战(玩家执黑)'),
                  value: GameMode.humanVsAIAsBlack,
                  groupValue: _gameMode,
                  onChanged: (value) {
                    if (value != null) {
                      setState(() {
                        _gameMode = value;
                      });
                    }
                  },
                ),
                RadioListTile<GameMode>(
                  title: const Text('⚪ 人机对战(玩家执白)'),
                  value: GameMode.humanVsAIAsWhite,
                  groupValue: _gameMode,
                  onChanged: (value) {
                    if (value != null) {
                      setState(() {
                        _gameMode = value;
                      });
                    }
                  },
                ),
                CheckboxListTile(
                  title: const Text('启用黑棋禁手(专业规则)'),
                  value: _forbiddenEnabled,
                  onChanged: (value) {
                    setState(() {
                      _forbiddenEnabled = value ?? false;
                    });
                  },
                ),
              ],
            ),
            actions: [
              TextButton(
                onPressed: () {
                  Navigator.of(context).pop();
                  _startNewGame();
                },
                child: const Text('确定'),
              ),
            ],
          );
        },
      ),
    );
  }

  void _startNewGame() {
    _board = List.generate(
      BOARD_SIZE,
      (_) => List.filled(BOARD_SIZE, 0),
    );
    _winner = null;
    _winningPath = [];
    _forbiddenPosition = null;
    _latestAIMove = null;

    if (_gameMode == GameMode.humanVsAIAsBlack) {
      _humanPlayer = 1;
      _aiPlayer = 2;
      _currentPlayer = 1;
    } else if (_gameMode == GameMode.humanVsAIAsWhite) {
      _humanPlayer = 2;
      _aiPlayer = 1;
      _currentPlayer = 1;
      WidgetsBinding.instance.addPostFrameCallback((_) {
        if (mounted && _currentPlayer == _aiPlayer && _winner == null) {
          _makeAIMove();
        }
      });
    } else {
      _currentPlayer = 1;
    }
  }

  // ===== 胜利检测 =====
  bool checkWin(int row, int col, int player) {
    for (var dir in directions) {
      int count = 1;
      int r = row + dir[0];
      int c = col + dir[1];
      while (r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE && _board[r][c] == player) {
        count++;
        r += dir[0];
        c += dir[1];
      }
      r = row - dir[0];
      c = col - dir[1];
      while (r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE && _board[r][c] == player) {
        count++;
        r -= dir[0];
        c -= dir[1];
      }
      if (count >= 5) {
        _winningPath = _getWinningPath(row, col, player);
        return true;
      }
    }
    return false;
  }

  List<Offset> _getWinningPath(int row, int col, int player) {
    for (var dir in directions) {
      final path = <Offset>[];
      path.add(Offset(col.toDouble(), row.toDouble()));
      int r = row + dir[0];
      int c = col + dir[1];
      while (r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE && _board[r][c] == player) {
        path.add(Offset(c.toDouble(), r.toDouble()));
        r += dir[0];
        c += dir[1];
      }
      r = row - dir[0];
      c = col - dir[1];
      while (r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE && _board[r][c] == player) {
        path.insert(0, Offset(c.toDouble(), r.toDouble()));
        r -= dir[0];
        c -= dir[1];
      }
      if (path.length >= 5) {
        return path.sublist(0, 5);
      }
    }
    return [];
  }

  // ===== 禁手检测 =====
  bool checkForbiddenMove(int row, int col) {
    if (_checkLongLine(row, col)) {
      _forbiddenType = '长连';
      return true;
    }
    final fourCount = _countFours(row, col);
    if (fourCount >= 2) {
      _forbiddenType = '四四';
      return true;
    }
    final liveThreeCount = _countLiveThrees(row, col);
    if (liveThreeCount >= 2) {
      _forbiddenType = '三三';
      return true;
    }
    return false;
  }

  bool _checkLongLine(int row, int col) {
    for (var dir in directions) {
      int count = 1;
      int r = row + dir[0];
      int c = col + dir[1];
      while (r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE && _board[r][c] == 1) {
        count++;
        r += dir[0];
        c += dir[1];
      }
      r = row - dir[0];
      c = col - dir[1];
      while (r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE && _board[r][c] == 1) {
        count++;
        r -= dir[0];
        c -= dir[1];
      }
      if (count >= 6) return true;
    }
    return false;
  }

  int _countFours(int row, int col) {
    int count = 0;
    for (var dir in directions) {
      final line = _extractLine(row, col, dir, 5);
      if (_isFour(line, 1)) count++;
    }
    return count;
  }

  bool _isFour(List<int> pattern, int player) {
    if (pattern.length != 5) return false;
    final selfCount = pattern.where((p) => p == player).length;
    final emptyCount = pattern.where((p) => p == 0).length;
    final opponentCount = pattern.where((p) => p == 3 - player).length;
    return selfCount == 4 && emptyCount == 1 && opponentCount == 0;
  }

  int _countLiveThrees(int row, int col) {
    int count = 0;
    for (var dir in directions) {
      final line = _extractLine(row, col, dir, 7);
      if (_isLiveThree(line, 1)) count++;
    }
    return count;
  }

  bool _isLiveThree(List<int> line, int player) {
    for (int i = 0; i <= 4; i++) {
      if (line[i] == 0 &&
          line[i + 1] == player &&
          line[i + 2] == player &&
          line[i + 3] == player &&
          line[i + 4] == 0) {
        return true;
      }
    }
    return false;
  }

  List<int> _extractLine(int row, int col, List<int> dir, int length) {
    final half = length ~/ 2;
    final line = <int>[];
    for (int i = -half; i <= half; i++) {
      final r = row + i * dir[0];
      final c = col + i * dir[1];
      if (r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE) {
        line.add(_board[r][c]);
      } else {
        line.add(2);
      }
    }
    return line;
  }

  // ===== AI 评分系统 =====
  int _evaluatePattern(List<int> pattern, int player) {
    final opponent = 3 - player;
    if (pattern.contains(opponent)) return 0;
    final selfCount = pattern.where((p) => p == player).length;
    final emptyCount = pattern.where((p) => p == 0).length;
    if (selfCount == 4 && emptyCount == 1) return 10000;
    if (selfCount == 3 && emptyCount == 2) return 2000;
    if (selfCount == 3 && emptyCount == 1) return 500;
    if (selfCount == 2 && emptyCount == 2) return 200;
    if (selfCount == 1) return 10;
    return 0;
  }

  List<int> _extractLineForScore(int row, int col, List<int> dir, int player) {
    final opponent = 3 - player;
    final line = <int>[];
    for (int i = -4; i <= 4; i++) {
      final r = row + i * dir[0];
      final c = col + i * dir[1];
      if (r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE) {
        line.add(_board[r][c]);
      } else {
        line.add(opponent); // 边界视为对手
      }
    }
    return line;
  }

  int _calculateScore(int row, int col, int player) {
    int totalScore = 0;
    for (var dir in directions) {
      final line = _extractLineForScore(row, col, dir, player);
      for (int i = 0; i <= 4; i++) {
        final segment = line.sublist(i, i + 5);
        totalScore += _evaluatePattern(segment, player);
      }
    }
    return totalScore;
  }

  // ===== AI 决策逻辑(已修复)=====
  void _makeAIMove() async {
    if (!mounted || _winner != null) return;

    await Future.delayed(const Duration(milliseconds: 600));

    // 检查是否全空
    bool isEmpty = true;
    for (var row in _board) {
      if (row.any((cell) => cell != 0)) {
        isEmpty = false;
        break;
      }
    }
    if (isEmpty) {
      _placeStone(7, 7, isAI: true);
      return;
    }

    // Step 1: 检查 AI 是否能直接获胜
    for (int row = 0; row < BOARD_SIZE; row++) {
      for (int col = 0; col < BOARD_SIZE; col++) {
        if (_board[row][col] != 0) continue;
        if (_aiPlayer == 1 && _forbiddenEnabled) {
          _board[row][col] = 1;
          final isForbidden = checkForbiddenMove(row, col);
          _board[row][col] = 0;
          if (isForbidden) continue;
        }
        if (_calculateScore(row, col, _aiPlayer) >= 10000) {
          _placeStone(row, col, isAI: true);
          return;
        }
      }
    }

    // Step 2: 检查是否需要防守(人类即将五连)
    for (int row = 0; row < BOARD_SIZE; row++) {
      for (int col = 0; col < BOARD_SIZE; col++) {
        if (_board[row][col] != 0) continue;
        if (_aiPlayer == 1 && _forbiddenEnabled) {
          _board[row][col] = 1;
          final isForbidden = checkForbiddenMove(row, col);
          _board[row][col] = 0;
          if (isForbidden) continue;
        }
        if (_calculateScore(row, col, _humanPlayer) >= 10000) {
          _placeStone(row, col, isAI: true);
          return;
        }
      }
    }

    // Step 3: 普通评估选最佳
    int bestScore = -1;
    int bestRow = -1, bestCol = -1;
    List<Offset> validMoves = [];

    for (int row = 0; row < BOARD_SIZE; row++) {
      for (int col = 0; col < BOARD_SIZE; col++) {
        if (_board[row][col] != 0) continue;

        if (_aiPlayer == 1 && _forbiddenEnabled) {
          _board[row][col] = 1;
          final isForbidden = checkForbiddenMove(row, col);
          _board[row][col] = 0;
          if (isForbidden) continue;
        }

        validMoves.add(Offset(row.toDouble(), col.toDouble()));

        final aiScore = _calculateScore(row, col, _aiPlayer);
        final opponentScore = _calculateScore(row, col, _humanPlayer);
        final score = aiScore + opponentScore;

        if (score > bestScore) {
          bestScore = score;
          bestRow = row;
          bestCol = col;
        }
      }
    }

    // Step 4: 落子(优先最佳,否则随机)
    if (bestRow != -1 && bestCol != -1) {
      _placeStone(bestRow, bestCol, isAI: true);
    } else if (validMoves.isNotEmpty) {
      final random = Random();
      final move = validMoves[random.nextInt(validMoves.length)];
      _placeStone(move.dx.toInt(), move.dy.toInt(), isAI: true);
    }
  }

  // ===== 落子逻辑 =====
  void _placeStone(int row, int col, {bool isAI = false}) {
    if (_winner != null || _board[row][col] != 0) return;

    final player = _currentPlayer;
    setState(() {
      _board[row][col] = player;

      if (player == 1 && _forbiddenEnabled) {
        if (checkForbiddenMove(row, col)) {
          _winner = 2;
          _forbiddenPosition = Offset(col.toDouble(), row.toDouble());
          Future.delayed(const Duration(milliseconds: 300), _showForbiddenDialog);
          return;
        }
      }

      if (checkWin(row, col, player)) {
        _winner = player;
        Future.delayed(const Duration(milliseconds: 500), _showWinDialog);
      } else {
        _currentPlayer = 3 - player;
        if (isAI) {
          _latestAIMove = Offset(col.toDouble(), row.toDouble());
          _aiAnimationController.forward().then((_) {
            if (mounted) {
              setState(() {
                _latestAIMove = null;
              });
            }
          });
        }
      }
    });

    if (_gameMode != GameMode.twoPlayer &&
        _currentPlayer == _aiPlayer &&
        _winner == null) {
      _makeAIMove();
    }
  }

  void _showWinDialog() {
    if (!mounted) return;
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: Text('${_winner == 1 ? '黑方' : '白方'}获胜!', textAlign: TextAlign.center),
        content: const Text('五子连珠,精彩对决!'),
        actions: [
          Center(
            child: ElevatedButton(
              onPressed: () {
                Navigator.of(context).pop();
                _startNewGame();
              },
              child: const Text('再来一局'),
            ),
          ),
        ],
      ),
    );
  }

  void _showForbiddenDialog() {
    if (!mounted) return;
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('❌ 黑棋禁手!', style: TextStyle(color: Colors.red)),
        content: Text('检测到 $_forbiddenType 禁手,白方获胜!'),
        actions: [
          Center(
            child: ElevatedButton(
              onPressed: () {
                Navigator.of(context).pop();
                _startNewGame();
              },
              child: const Text('重新开始'),
            ),
          ),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.transparent,
        elevation: 0,
        title: const Text('青绿五子棋', style: TextStyle(color: Colors.black)),
        centerTitle: true,
        actions: [
          IconButton(
            icon: const Icon(Icons.settings, color: Colors.black),
            onPressed: () {
              if (mounted) _showModeSelection();
            },
          ),
        ],
      ),
      body: Container(
        decoration: BoxDecoration(
          gradient: LinearGradient(
            begin: Alignment.topCenter,
            end: Alignment.bottomCenter,
            colors: [BACKGROUND_START, BACKGROUND_END],
          ),
        ),
        child: Center(
          child: LayoutBuilder(
            builder: (context, constraints) {
              final minSize = min(constraints.maxWidth, constraints.maxHeight) - 2 * PADDING;
              _cellSize = minSize / (BOARD_SIZE - 1);

              return GestureDetector(
                onTapDown: (details) {
                  if (_gameMode != GameMode.twoPlayer && _currentPlayer != _humanPlayer) {
                    return;
                  }
                  final renderBox = context.findRenderObject() as RenderBox;
                  final local = renderBox.globalToLocal(details.globalPosition);
                  final dx = local.dx - PADDING;
                  final dy = local.dy - PADDING;
                  if (dx >= 0 && dy >= 0) {
                    final col = (dx / _cellSize).round();
                    final row = (dy / _cellSize).round();
                    if (row >= 0 && row < BOARD_SIZE && col >= 0 && col < BOARD_SIZE) {
                      _placeStone(row, col);
                    }
                  }
                },
                child: Stack(
                  children: [
                    CustomPaint(
                      painter: _GomokuBoardPainter(
                        cellSize: _cellSize,
                        winningPath: _winningPath,
                        forbiddenPosition: _forbiddenPosition,
                      ),
                      child: SizedBox.square(dimension: minSize + 2 * PADDING),
                    ),
                    ..._renderStones(),
                  ],
                ),
              );
            },
          ),
        ),
      ),
    );
  }

  List<Widget> _renderStones() {
    final stones = <Widget>[];
    for (int row = 0; row < BOARD_SIZE; row++) {
      for (int col = 0; col < BOARD_SIZE; col++) {
        final player = _board[row][col];
        if (player != 0) {
          final x = PADDING + col * _cellSize - _cellSize * 0.4;
          final y = PADDING + row * _cellSize - _cellSize * 0.4;

          bool isForbidden = (player == 1 &&
              _forbiddenPosition != null &&
              _forbiddenPosition!.dx == col &&
              _forbiddenPosition!.dy == row);

          Widget stone = _buildStone(player, isForbidden: isForbidden);

          if (player == _aiPlayer &&
              _latestAIMove != null &&
              _latestAIMove!.dx == col &&
              _latestAIMove!.dy == row) {
            stone = AnimatedOpacity(
              opacity: _aiOpacity.value,
              duration: const Duration(milliseconds: 300),
              child: stone,
            );
          }

          stones.add(Positioned(left: x, top: y, child: stone));
        }
      }
    }
    return stones;
  }

  Widget _buildStone(int player, {bool isForbidden = false}) {
    if (isForbidden) {
      return AnimatedContainer(
        duration: const Duration(milliseconds: 800),
        curve: Curves.easeInOut,
        decoration: BoxDecoration(
          color: Colors.red,
          shape: BoxShape.circle,
          boxShadow: [
            BoxShadow(
              color: Colors.red.withOpacity(0.5),
              blurRadius: 12,
              spreadRadius: 2,
            ),
          ],
        ),
        width: _cellSize * 0.8,
        height: _cellSize * 0.8,
      );
    }

    final isBlack = player == 1;
    return Container(
      width: _cellSize * 0.8,
      height: _cellSize * 0.8,
      decoration: BoxDecoration(
        color: isBlack ? const Color(0xFF222222) : Colors.white,
        shape: BoxShape.circle,
        boxShadow: [
          BoxShadow(
            color: isBlack ? Colors.black26 : Colors.grey.withOpacity(0.3),
            blurRadius: 8,
            offset: const Offset(0, 2),
          ),
          if (!isBlack)
            BoxShadow(
              color: Colors.white.withOpacity(0.6),
              blurRadius: 2,
              offset: const Offset(-1, -1),
              spreadRadius: -1,
            ),
        ],
      ),
    );
  }
}

class _GomokuBoardPainter extends CustomPainter {
  final double cellSize;
  final List<Offset> winningPath;
  final Offset? forbiddenPosition;

  _GomokuBoardPainter({
    required this.cellSize,
    required this.winningPath,
    this.forbiddenPosition,
  });

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = const Color(0xFF888888)
      ..strokeWidth = 1.2
      ..style = PaintingStyle.stroke
      ..strokeCap = StrokeCap.round;

    final offset = 24.0;
    final boardSize = 15;

    for (int i = 0; i < boardSize; i++) {
      final y = offset + i * cellSize;
      canvas.drawLine(
        Offset(offset, y),
        Offset(offset + (boardSize - 1) * cellSize, y),
        paint,
      );
    }
    for (int j = 0; j < boardSize; j++) {
      final x = offset + j * cellSize;
      canvas.drawLine(
        Offset(x, offset),
        Offset(x, offset + (boardSize - 1) * cellSize),
        paint,
      );
    }

    if (winningPath.length >= 5) {
      final winPaint = Paint()
        ..color = Colors.red.withOpacity(0.7)
        ..strokeWidth = 4
        ..style = PaintingStyle.stroke
        ..strokeCap = StrokeCap.round;

      final points = winningPath.map((offset) {
        return Offset(
          24.0 + offset.dx * cellSize,
          24.0 + offset.dy * cellSize,
        );
      }).toList();

      for (int i = 0; i < points.length - 1; i++) {
        canvas.drawLine(points[i], points[i + 1], winPaint);
      }
    }

    if (forbiddenPosition != null) {
      final x = 24.0 + forbiddenPosition!.dx * cellSize;
      final y = 24.0 + forbiddenPosition!.dy * cellSize;
      final forbiddenPaint = Paint()
        ..color = Colors.red.withOpacity(0.6)
        ..style = PaintingStyle.stroke
        ..strokeWidth = 4;
      canvas.drawCircle(Offset(x, y), cellSize * 0.5, forbiddenPaint);
    }
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}

运行界面

结语:专业规则,成就竞技之美

通过实现 RIF 禁手规则,我们的青绿五子棋应用完成了从"休闲玩具"到"竞技平台"的关键跃迁。三三、四四、长连的精准判定,不仅体现了对专业规则的尊重,更让玩家在实战中理解五子棋的深层策略------如何利用禁手设陷阱,又如何避免落入对手的圈套。

这套实现充分展现了 Flutter + OpenHarmony 的工程能力

  • 算法精准:禁手检测覆盖所有边界情况
  • 体验流畅:红色闪烁 + 弹窗提示,反馈即时明确
  • 架构灵活:禁手开关无缝集成,不影响原有逻辑
  • AI 适配:AI 自动规避禁手,行为更真实
相关推荐
微祎_9 小时前
Flutter for OpenHarmony:链迹 - 基于Flutter的会话级快速链接板极简实现方案
flutter
微祎_10 小时前
Flutter for OpenHarmony:魔方计时器开发实战 - 基于Flutter的专业番茄工作法应用实现与交互设计
flutter·交互
空白诗15 小时前
基础入门 Flutter for Harmony:Text 组件详解
javascript·flutter·harmonyos
喝拿铁写前端16 小时前
接手老 Flutter 项目踩坑指南:从环境到调试的实际经验
前端·flutter
renke336416 小时前
Flutter for OpenHarmony:单词迷宫 - 基于路径探索与字母匹配的认知解谜系统
flutter
火柴就是我17 小时前
我们来尝试实现一个类似内阴影的效果
android·flutter
ZH154558913117 小时前
Flutter for OpenHarmony Python学习助手实战:数据科学工具库的实现
python·学习·flutter
左手厨刀右手茼蒿17 小时前
Flutter for OpenHarmony 实战:Barcode — 纯 Dart 条形码与二维码生成全指南
android·flutter·ui·华为·harmonyos
铅笔侠_小龙虾18 小时前
Flutter 学习目录
学习·flutter
子春一19 小时前
Flutter for OpenHarmony:箱迹 - 基于 Flutter 的轻量级包裹追踪系统实现与状态管理实践
flutter