表演赛,难度裁判和表现裁判分计分错误

This commit is contained in:
马梓彭 2026-09-22 15:48:51 +08:00
parent 51ed62dfe5
commit 18b1f36de5
6 changed files with 87 additions and 18 deletions

View File

@ -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) =>

View File

@ -30,7 +30,7 @@ class CompetitionRuleCalculator {
/// 当前单名表现裁判给出的加权原始分。
///
/// [minusCounts]只包含裁判手动减号;失误自动生成的减号由 [errors]
/// 按规则公式扣除。页面可以把两者相加展示,但写入平台时必须分开。
/// 按规则公式扣除。页面展示与写入平台时都必须把两者分开。
///
/// 裁判端只展示该分数;裁判间平均以及项目系数由平台汇总时处理。
static double performanceRaw(

View File

@ -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<double>(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<String, int> 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<double>(
0,
(sum, category) => sum + showDifficultyCategorySubtotal(category),
);
}
if (profile.competition == CompetitionKind.doubleDutch) {
return profile.roleCode == 'DT'
? CompetitionRuleCalculator.doubleDutchTurner(difficultyCounts)

View File

@ -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<SubmissionScoreDetail> _submissionScoreDetails(
SubmissionScoreDetail(
label: '失误',
value: '${controller.performanceErrorCount.value} 次',
description: '已自动计入各表现类别减数量',
description: '计分时从五个表现类别各减去该次数',
),
];
case ScoreRole.creative:

View File

@ -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},

View File

@ -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名表现裁判按实际人数取平均'),
);
});
}