diff --git a/lib/core/models/elite_scoring_rule.dart b/lib/core/models/elite_scoring_rule.dart index f716fbe..bffcf64 100644 --- a/lib/core/models/elite_scoring_rule.dart +++ b/lib/core/models/elite_scoring_rule.dart @@ -1,3 +1,5 @@ +import 'dart:math' as math; + import 'judge_account.dart'; abstract final class EliteScoringRuleCatalog { @@ -48,6 +50,18 @@ abstract final class EliteScoringRuleCatalog { '8': 2.56, }; + /// 表演赛中,个人绳、车轮跳、交互绳沿用个人花样换算表; + /// 长绳、旅行跳按脑图规定使用 L(x)=1.5^x。 + static double showDifficultyWeight(String category, String level) { + if (category.contains('长绳') || category.contains('旅行跳')) { + final value = math.pow(1.5, double.parse(level)).toDouble(); + // 服务端 ScoreRuleItem.UnitValue 为 DECIMAL(18,4),离线回退使用相同精度, + // 避免平板预览与正式提交后的服务端结果出现浮点尾差。 + return double.parse(value.toStringAsFixed(4)); + } + return standardDifficultyWeights[level] ?? 0; + } + static const individualSolo = 'individualSolo'; static const individualDuo = 'individualDuo'; static const individualTeam4 = 'individualTeam4'; @@ -83,20 +97,22 @@ abstract final class EliteScoringRuleCatalog { : 'DT摇绳难度:L(x)=1.5^x,取最高20个难度动作的加权总和并除以20。', CompetitionKind.show => profile.roleCode == 'A' - ? 'A组裁判记录个人绳力量体操、多摇、车轮跳、交互绳和旅行跳难度;三名A组裁判取平均。' - : 'B组裁判记录个人绳绳子运用、车轮跳、交互绳和长绳难度;三名B组裁判取平均。', + ? 'A组裁判记录个人绳力量体操、多摇、车轮跳、交互绳和旅行跳难度;旅行跳按L(x)=1.5^x换算,其余沿用个人花样换算表;1至3名A组裁判按实际人数取平均。' + : 'B组裁判记录个人绳绳子运用、车轮跳、交互绳和长绳难度;长绳按L(x)=1.5^x换算,其余沿用个人花样换算表;1至3名B组裁判按实际人数取平均。', CompetitionKind.prescribed => '', }; } static String performanceGuidance(JudgeAccount profile) { - final judgeCount = profile.competition == CompetitionKind.show ? 3 : 2; + final judgeDescription = profile.competition == CompetitionKind.show + ? '1至3名表现裁判按实际人数取平均' + : '2名裁判平均'; final multiplier = profile.competition == CompetitionKind.doubleDutch ? '0.033' : '0.05'; return '音乐20%、完成质量25%、娱乐25%、创意15%、多样性15%;' - '每记录1次失误,五项评分同步减1。本页仅显示当前裁判原始评分;' - '$judgeCount名裁判平均及×$multiplier由平台汇总。'; + '减数量仅记录裁判手工减号,每1次失误再从五项评分各减1。本页仅显示当前裁判原始评分;' + '$judgeDescription,再×$multiplier由平台汇总。'; } static String chiefGuidance(JudgeAccount profile) => diff --git a/lib/core/rule_engine/calculators/competition_rule_calculator.dart b/lib/core/rule_engine/calculators/competition_rule_calculator.dart index 6863da2..65218f5 100644 --- a/lib/core/rule_engine/calculators/competition_rule_calculator.dart +++ b/lib/core/rule_engine/calculators/competition_rule_calculator.dart @@ -30,7 +30,7 @@ class CompetitionRuleCalculator { /// 当前单名表现裁判给出的加权原始分。 /// /// [minusCounts]只包含裁判手动减号;失误自动生成的减号由 [errors] - /// 按规则公式扣除。页面可以把两者相加展示,但写入平台时必须分开。 + /// 按规则公式扣除。页面展示与写入平台时都必须把两者分开。 /// /// 裁判端只展示该分数;裁判间平均以及项目系数由平台汇总时处理。 static double performanceRaw( diff --git a/lib/modules/scoring/controllers/scoring_controller.dart b/lib/modules/scoring/controllers/scoring_controller.dart index 70a222b..4e1991e 100644 --- a/lib/modules/scoring/controllers/scoring_controller.dart +++ b/lib/modules/scoring/controllers/scoring_controller.dart @@ -404,7 +404,9 @@ class ScoringController extends GetxController { } else if (key != null && item.adjustSign < 0) { performanceMinusCounts[key] = count; _itemUiKeys[item.code] = key; - } else if (key != null && item.inputMode == 'score') { + } else if (key != null && + !item.isAutoGenerated && + item.inputMode == 'score') { final rounded = score.round().clamp(0, 24); performancePlusCounts[key] = (rounded - 12).clamp(0, 12); performanceMinusCounts[key] = (12 - rounded).clamp(0, 12); @@ -708,9 +710,22 @@ class ScoringController extends GetxController { double showDifficultyCategorySubtotal(String category) => showDifficultyLevels.fold(0, (sum, level) { final count = showDifficultyCount(category, level); - return sum + (rule.difficultyWeights[level] ?? 0) * count; + return sum + _showDifficultyUnit(category, level) * count; }); + double _showDifficultyUnit(String category, String level) { + if (apiScoreSheet != null) { + for (final item in apiScoreSheet!.items) { + if (_exactDifficultyLevelOf(item) != level) continue; + final matched = _matchingKey([ + category, + ], '${item.subjectName ?? ''}${item.name}${item.code}'); + if (matched != null) return item.unitValue; + } + } + return EliteScoringRuleCatalog.showDifficultyWeight(category, level); + } + String _showDifficultyKey(String category, String level) => '$category::$level'; @@ -748,9 +763,9 @@ class ScoringController extends GetxController { int performanceMinusCount(String item) => performanceMinusCounts[item] ?? 0; - /// 页面展示的减数量包含裁判手动减号和失误自动产生的减号。 + /// 页面减数量只展示裁判手工减号;失误在公式中另行扣减。 int performanceEffectiveMinusCount(String item) => - performanceMinusCount(item) + performanceErrorCount.value; + performanceMinusCount(item); Map get performanceEffectiveMinusCounts => { for (final item in performanceItems.keys) @@ -783,10 +798,7 @@ class ScoringController extends GetxController { } void setPerformanceEffectiveMinusCount(String item, int count) { - final manualCount = (count - performanceErrorCount.value) - .clamp(0, 99) - .toInt(); - performanceMinusCounts[item] = manualCount; + performanceMinusCounts[item] = count.clamp(0, 99).toInt(); _syncPerformanceItemScore(item); recalculate(); } @@ -821,7 +833,7 @@ class ScoringController extends GetxController { }) .join('+'); return '单项=加数量+12-人工减数量-失误数量(限0~24整数)\n' - '失误${performanceErrorCount.value}次 → 页面五项减数量各自动+' + '失误${performanceErrorCount.value}次 → 公式中五项各自额外减' '${performanceErrorCount.value}\n' '当前减/加:$markDetails\n' '单项分:$itemScores\n' @@ -951,6 +963,12 @@ class ScoringController extends GetxController { double _difficultyScore() { if (profile.role != ScoreRole.difficulty) return 0; if (apiScoreSheet != null) return _apiDifficultyScore(); + if (profile.competition == CompetitionKind.show) { + return showDifficultyCategories.fold( + 0, + (sum, category) => sum + showDifficultyCategorySubtotal(category), + ); + } if (profile.competition == CompetitionKind.doubleDutch) { return profile.roleCode == 'DT' ? CompetitionRuleCalculator.doubleDutchTurner(difficultyCounts) diff --git a/lib/modules/scoring/views/scoring_page.dart b/lib/modules/scoring/views/scoring_page.dart index 21fc8fc..440840a 100644 --- a/lib/modules/scoring/views/scoring_page.dart +++ b/lib/modules/scoring/views/scoring_page.dart @@ -1169,7 +1169,7 @@ class _PerformancePanel extends StatelessWidget { children: [ _PerformanceMarkCount( value: minusCount, - semanticLabel: '${entry.key}减号数量,包含失误自动减号', + semanticLabel: '${entry.key}裁判手工减号数量', color: hasErrors ? AppColors.danger : AppColors.primary, onTap: controller.isReadOnly ? null @@ -2475,7 +2475,7 @@ List _submissionScoreDetails( SubmissionScoreDetail( label: '失误', value: '${controller.performanceErrorCount.value} 次', - description: '已自动计入各表现类别减数量', + description: '计分时从五个表现类别各减去该次数', ), ]; case ScoreRole.creative: diff --git a/test/competition_rule_calculator_test.dart b/test/competition_rule_calculator_test.dart index bb48c95..f570261 100644 --- a/test/competition_rule_calculator_test.dart +++ b/test/competition_rule_calculator_test.dart @@ -22,6 +22,17 @@ void main() { expect(score, closeTo(14.4, .0001)); }); + test('表演赛表现裁判的手工减数量与失误次数分别扣减', () { + final score = CompetitionRuleCalculator.performanceRaw( + const {'音乐': 0, '完成质量': 0, '娱乐': 0, '创意': 0, '多样性': 0}, + // 脑图公式顺序对应单项分:音乐9、创意10、多样性8、娱乐8、完成质量8。 + minusCounts: const {'音乐': 1, '完成质量': 2, '娱乐': 2, '创意': 0, '多样性': 2}, + errors: 2, + ); + + expect(score, closeTo(8.5, .000001)); + }); + test('交互绳表现使用0.033系数', () { final result = CompetitionRuleCalculator.performance( const {'音乐': 0, '完成质量': 0, '娱乐': 0, '创意': 0, '多样性': 0}, @@ -59,7 +70,7 @@ void main() { ); }); - test('失误自动生成减数量后仍按公式单独减去失误数量', () { + test('已有手工减数量时仍按公式单独减去失误数量', () { final withoutError = CompetitionRuleCalculator.performanceRaw( const {'音乐': 3, '完成质量': 3, '娱乐': 3, '创意': 3, '多样性': 3}, minusCounts: const {'音乐': 2, '完成质量': 2, '娱乐': 2, '创意': 2, '多样性': 2}, diff --git a/test/elite_scoring_rule_test.dart b/test/elite_scoring_rule_test.dart index 16dead4..99c855a 100644 --- a/test/elite_scoring_rule_test.dart +++ b/test/elite_scoring_rule_test.dart @@ -82,6 +82,18 @@ void main() { ]); }); + test('表演赛长绳和旅行跳使用1.5指数换算,其余类别沿用个人花样表', () { + expect( + EliteScoringRuleCatalog.showDifficultyWeight('DP+DM(个人绳)', '3'), + .34, + ); + expect(EliteScoringRuleCatalog.showDifficultyWeight('DT(旅行跳)', '3'), 3.375); + expect( + EliteScoringRuleCatalog.showDifficultyWeight('DL(长绳)', '0.5'), + 1.2247, + ); + }); + test('各精英级裁判具有对应规则提示', () { for (final account in ['IF-DP1', 'WH-DA1', 'DD-DJ1', 'SH-A1']) { expect( @@ -100,4 +112,16 @@ void main() { contains('空间违例'), ); }); + + test('表演赛规则提示按实际1至3名裁判取平均', () { + final difficulty = JudgeAccounts.find('SH-A1')!; + expect( + EliteScoringRuleCatalog.difficultyGuidance(difficulty), + contains('1至3名A组裁判按实际人数取平均'), + ); + expect( + EliteScoringRuleCatalog.performanceGuidance(JudgeAccounts.find('SH-P1')!), + contains('1至3名表现裁判按实际人数取平均'), + ); + }); }