《乒乓球电子裁判:基于 Flutter for OpenHarmony 的发球检测系统》

🏓《乒乓球电子裁判:基于 Flutter for OpenHarmony 的发球检测系统》

🌐 加入社区

欢迎加入 开源鸿蒙跨平台开发者社区,获取最新资源与技术支持!


一、引言:为什么需要"电子发球裁判"?

在业余乒乓球比赛中,发球违例(如遮挡、抛球不足16cm、非垂直抛球)是最常见也最难判定的争议点。专业裁判需经过严格训练,而普通玩家往往只能"凭感觉"判罚,影响公平性。

借助 Flutter for OpenHarmony ,我们可以在平板或智慧屏设备上部署一个辅助裁判系统 :通过手动标记发球动作,自动记录发球方、轮次、违例次数,并提示当前应由谁发球。

用户只需点击"A 发球 OK"、"B 发球违规",系统即可自动推进回合、统计数据------界面只有文字和按钮,却能显著提升比赛规范性。


二、系统目标:轻量、离线、规则准确

本系统不依赖摄像头或 AI,而是基于 ITTF(国际乒联)简化规则,实现以下功能:

  • ✅ 支持双打/单打(默认单打)
  • ✅ 每2分自动交换发球权
  • ✅ 11分制,需领先2分获胜(最多打至15分)
  • ✅ 手动标记"有效发球"或"发球违例"
  • ✅ 实时显示当前发球方、比分、局数

💡 设计理念
不替代人眼,而是辅助记忆与规则执行。裁判/球员主动操作,系统负责计数与提醒。


三、核心状态设计

1. 数据结构

dart 复制代码
class MatchState {
  // 当前局
  int currentSet = 1;
  // 比分 [A, B]
  List<int> scores = [0, 0];
  // 每局历史 [[A1,B1], [A2,B2], ...]
  List<List<int>> setHistory = [];
  
  // 发球控制
  bool isTeamAServing = true; // true=A发球,false=B发球
  int serveCount = 0;         // 当前发球方已连续发球次数(0~1)

  // 违例统计
  int foulCountA = 0;
  int foulCountB = 0;
}

2. 发球轮换逻辑

根据 ITTF 规则:

  • 每方连续发2球后交换发球权
  • 局末平分(10:10)后,每1球交换发球
dart 复制代码
void advanceServe() {
  serveCount++;
  if ((scores[0] < 10 || scores[1] < 10) && serveCount >= 2) {
    // 常规阶段:2球换发
    _switchServe();
  } else if (scores[0] >= 10 && scores[1] >= 10) {
    // deuce 阶段:1球换发
    _switchServe();
  }
}

void _switchServe() {
  isTeamAServing = !isTeamAServing;
  serveCount = 0;
}

3. 得分处理

dart 复制代码
void addPoint(String team) {
  final index = team == 'A' ? 0 : 1;
  scores[index]++;

  // 检查是否赢下当前局
  if (_checkSetWin()) {
    setHistory.add([...scores]);
    if (setHistory.length < 3) { // 最多3局
      scores = [0, 0];
      isTeamAServing = setHistory.length.isOdd; // 交替先发
      serveCount = 0;
    }
  } else {
    advanceServe(); // 正常得分后推进发球
  }
}

bool _checkSetWin() {
  final a = scores[0], b = scores[1];
  if (a >= 11 || b >= 11) {
    return (a - b).abs() >= 2 || a == 15 || b == 15;
  }
  return false;
}

四、交互设计:极简裁判面板

界面分为三部分:

  1. 当前局比分 + 发球提示
  2. 发球操作区(标记有效/违例)
  3. 历史局分 + 违例统计

所有操作均为手动点击 ,避免误触,确保裁判可控性。


五、完整可运行代码(lib/main.dart)

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

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'PingPong Referee - OH',
      home: Scaffold(body: PingPongReferee()),
    );
  }
}

class PingPongReferee extends StatefulWidget {
  @override
  State<PingPongReferee> createState() => _PingPongRefereeState();
}

class _PingPongRefereeState extends State<PingPongReferee> {
  int currentSet = 1;
  List<int> scores = [0, 0]; // [A, B]
  List<List<int>> setHistory = []; // e.g., [[11,8], [9,11]]
  bool isTeamAServing = true;
  int serveCount = 0;
  int foulCountA = 0;
  int foulCountB = 0;

  void resetMatch() {
    setState(() {
      currentSet = 1;
      scores = [0, 0];
      setHistory.clear();
      isTeamAServing = true;
      serveCount = 0;
      foulCountA = 0;
      foulCountB = 0;
    });
  }

  void recordValidServe(String team) {
    // 有效发球 → 对方失分(即本方得分)
    addPoint(team);
  }

  void recordFoulServe(String team) {
    // 发球违例 → 对方直接得分
    final opponent = team == 'A' ? 'B' : 'A';
    addPoint(opponent);
    // 记录违例
    setState(() {
      if (team == 'A') foulCountA++;
      else foulCountB++;
    });
  }

  void addPoint(String team) {
    setState(() {
      final idx = team == 'A' ? 0 : 1;
      scores[idx]++;

      // 检查是否赢下当前局
      if (_checkSetWin()) {
        setHistory.add([scores[0], scores[1]]);
        if (setHistory.length < 3) {
          // 开始新局
          scores = [0, 0];
          currentSet = setHistory.length + 1;
          // 交替先发球权
          isTeamAServing = (setHistory.length % 2 == 1);
          serveCount = 0;
        }
      } else {
        _advanceServe();
      }
    });
  }

  bool _checkSetWin() {
    final a = scores[0], b = scores[1];
    if (a >= 11 || b >= 11) {
      return (a - b).abs() >= 2 || a == 15 || b == 15;
    }
    return false;
  }

  void _advanceServe() {
    serveCount++;
    if ((scores[0] < 10 || scores[1] < 10) && serveCount >= 2) {
      _switchServe();
    } else if (scores[0] >= 10 && scores[1] >= 10) {
      _switchServe();
    }
  }

  void _switchServe() {
    isTeamAServing = !isTeamAServing;
    serveCount = 0;
  }

  @override
  Widget build(BuildContext context) {
    final servingTeam = isTeamAServing ? 'A' : 'B';
    final nextServeInfo = '当前发球: Team $servingTeam (第 ${serveCount + 1} 球)';

    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: Column(
        children: [
          // 标题
          const Text('🏓 乒乓球电子裁判', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
          const SizedBox(height: 10),

          // 当前局比分
          Text('第 $currentSet 局', style: const TextStyle(fontSize: 20)),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              Text('A\n${scores[0]}', style: const TextStyle(fontSize: 48, fontWeight: FontWeight.bold)),
              Text('B\n${scores[1]}', style: const TextStyle(fontSize: 48, fontWeight: FontWeight.bold)),
            ],
          ),
          const SizedBox(height: 10),
          Text(nextServeInfo, style: const TextStyle(fontSize: 18, color: Colors.blue)),

          const SizedBox(height: 30),

          // 发球操作区
          const Text('发球判定', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
          const SizedBox(height: 10),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              Column(
                children: [
                  const Text('Team A'),
                  Row(children: [
                    ElevatedButton(
                      onPressed: () => recordValidServe('A'),
                      child: const Text('有效'),
                      style: ElevatedButton.styleFrom(backgroundColor: Colors.green),
                    ),
                    const SizedBox(width: 10),
                    ElevatedButton(
                      onPressed: () => recordFoulServe('A'),
                      child: const Text('违例'),
                      style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
                    ),
                  ]),
                ],
              ),
              Column(
                children: [
                  const Text('Team B'),
                  Row(children: [
                    ElevatedButton(
                      onPressed: () => recordValidServe('B'),
                      child: const Text('有效'),
                      style: ElevatedButton.styleFrom(backgroundColor: Colors.green),
                    ),
                    const SizedBox(width: 10),
                    ElevatedButton(
                      onPressed: () => recordFoulServe('B'),
                      child: const Text('违例'),
                      style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
                    ),
                  ]),
                ],
              ),
            ],
          ),

          const SizedBox(height: 20),

          // 历史与统计
          if (setHistory.isNotEmpty) ...[
            const Text('历史局分', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
            Wrap(
              spacing: 10,
              children: setHistory.asMap().entries.map((e) {
                final set = e.key + 1;
                final a = e.value[0];
                final b = e.value[1];
                return Chip(label: Text('第$set局: $a-$b'));
              }).toList(),
            ),
          ],
          const SizedBox(height: 10),
          Text('违例统计: A=$foulCountA, B=$foulCountB', style: const TextStyle(color: Colors.orange)),

          const Spacer(),

          // 重置按钮
          ElevatedButton.icon(
            onPressed: resetMatch,
            icon: const Icon(Icons.refresh),
            label: const Text('重置比赛'),
          ),
        ],
      ),
    );
  }
}

六、OpenHarmony 实测说明

  1. 创建项目 :DevEco Studio → 新建 Flutter for OpenHarmony 项目

  2. 替换代码 :将上述代码粘贴至 lib/main.dart

  3. 依赖检查 :确保 oh-package.json5 包含:

    json5 复制代码
    "dependencies": {
      "@ohos/flutter_ohos": "file:./har/flutter.har"
    }
  4. 运行 :执行 ohpm install 后点击 ▶️

操作流程

  • 比赛开始 → 系统提示 "当前发球: Team A"
  • A 发球成功 → 点击 "A 有效" → 若 B 未接回,则再点一次 "A 有效" 得分
  • A 发球遮挡 → 点击 "A 违例" → B 直接得1分
  • 每2分自动切换发球方,10:10后每1分切换

七、结语:用技术守护体育精神

本系统虽无 AI 视觉识别,但通过规则内嵌 + 人工确认 的方式,在 OpenHarmony 设备上实现了可靠的发球辅助裁判功能。它证明了:即使是简单的状态机,也能在体育场景中创造真实价值。

相关推荐
微祎_19 小时前
Flutter for OpenHarmony:链迹 - 基于Flutter的会话级快速链接板极简实现方案
flutter
微祎_19 小时前
Flutter for OpenHarmony:魔方计时器开发实战 - 基于Flutter的专业番茄工作法应用实现与交互设计
flutter·交互
x***r15121 小时前
SuperScan4单文件扫描安装步骤详解(附端口扫描与主机存活检测教程)
windows
不爱学习的老登1 天前
Windows客户端与Linux服务器配置ssh无密码登录
linux·服务器·windows
陌陌龙1 天前
全免去水印大师 v1.7.6 | 安卓端高效水印处理神器
windows
csdn2015_1 天前
将object转换成list
开发语言·windows·python
空白诗1 天前
基础入门 Flutter for Harmony:Text 组件详解
javascript·flutter·harmonyos
喝拿铁写前端1 天前
接手老 Flutter 项目踩坑指南:从环境到调试的实际经验
前端·flutter
renke33641 天前
Flutter for OpenHarmony:单词迷宫 - 基于路径探索与字母匹配的认知解谜系统
flutter
火柴就是我1 天前
我们来尝试实现一个类似内阴影的效果
android·flutter