《乒乓球电子裁判:基于 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 设备上实现了可靠的发球辅助裁判功能。它证明了:即使是简单的状态机,也能在体育场景中创造真实价值。

相关推荐
2601_9623649711 分钟前
Windows Hello 密钥生命周期科普:注册、鉴权、重置加密逻辑
windows·电脑
脚踏实地皮皮晨11 分钟前
003003002_WPF Grid 基类官方类定义逐行深度解析
开发语言·windows·算法·c#·wpf·visual studio
末代iOS程序员华仔18 分钟前
Flutter开发iOS海外工具类应用上架合规指南与风险规避
flutter·ios
笨鸟先飞,勤能补拙11 小时前
AI 赋能网络安全领域深度剖析
网络·人工智能·windows·安全·web安全·网络安全·github
蜡台12 小时前
Flutter 环境搭建 Dart配置
flutter·dart
我上去就是一套QWER然后等待复活12 小时前
Windows 清理 CC Switch 并恢复官方 Codex CLI 登录流程(完整记录)
windows
吾儿良辰13 小时前
一个被BCL遗忘的高性能集合:C# CircularBuffer<T>深度解析
开发语言·windows·c#
哎呦喂我去去去13 小时前
C#实现屏幕墙:同时监控多个电脑桌面(支持Windows、信创Linux、银河麒麟、统信UOS)
linux·windows·c#
脚踏实地皮皮晨16 小时前
002002002_DepandencyObject类2
开发语言·windows·算法·c#·visual studio
驱动小百科17 小时前
Windows运行库合集下载 VC++、DirectX、.NET运行库安装教程
c++·windows·.net·windows运行库合集下载·windows运行库安装