YD_Event.Admin/YD_Event.Application/JudgeScore/ShowcaseRuleDefinition.cs

172 lines
8.9 KiB
C#
Raw Normal View History

2026-08-23 16:21:49 +08:00
namespace YD_Event.Application.JudgeScore;
/// <summary>
/// 表演赛(个人花样基础套路赛)内置计分规则定义。
/// <para>
/// 只在初始化/同步规则库时使用;运行期的评分项一律从 ScoreRule / ScoreRuleItem 读取,
/// 后台调整评分项、难度分值、席位配置后无需改代码。
/// </para>
/// <para>
/// 计分口径(裁判端原型 + 花样跳绳裁判端逻辑与测试验收说明书 6.7):<br/>
/// 席位:难度A组×3、难度B组×3、表现P×3、主裁T×1。<br/>
/// A 组裁判一人评 个人绳(DP+DM)、车轮跳(DA)、交互绳(DT_INTERACTION)、旅行跳(DT_TRAVEL) 四个类别,A=average(A1,A2,A3);<br/>
/// B 组裁判一人评 个人绳(DR)、车轮跳(DB)、交互绳(DJ)、长绳(DL) 四个类别,B=average(B1,B2,B3);<br/>
/// 总难度 D=(A+B)/2;表现 P=average(p1,p2,p3)×0.05;<br/>
/// 规定元素 Q=max(0,1-0.05×缺失类别数),5 类为 个人绳、车轮跳、交互绳、长绳(6米以上)、旅行跳;<br/>
/// 扣分 M=max(0,1-(失误+违例)),表演赛不记间歇与空间违例;<br/>
/// 总分 R=max(0, D×(1+P)×Q×M)。
/// </para>
/// </summary>
public static class ShowcaseRuleDefinition
{
/// <summary>
/// 表演赛规则编码
/// </summary>
public const string RuleCode = "ROPE_SHOWCASE";
/// <summary>
/// 表演赛规则名称
/// </summary>
public const string RuleName = "表演赛计分规则";
/// <summary>
/// A 组难度裁判角色类别编码。一人评个人绳、车轮跳、交互绳、旅行跳四个类别。
/// </summary>
public const string RoleDifficultyGroupA = "DGA";
/// <summary>
/// B 组难度裁判角色类别编码。一人评个人绳、车轮跳、交互绳、长绳四个类别。
/// </summary>
public const string RoleDifficultyGroupB = "DGB";
/// <summary>
/// 表现裁判角色类别编码
/// </summary>
public const string RolePerformance = FreestyleRuleDefinition.RolePerformance;
/// <summary>
/// 主裁判角色类别编码
/// </summary>
public const string RoleChief = FreestyleRuleDefinition.RoleChief;
/// <summary>
/// 规定元素目标类别数:个人绳、车轮跳、交互绳、长绳(6米以上)、旅行跳
/// </summary>
public const decimal RequiredTarget = 5m;
/// <summary>
/// A 组难度裁判要评的类别。
/// 逻辑说明书要求把脑图里两个同名 DT 拆成 DT_INTERACTION(交互绳)与 DT_TRAVEL(旅行跳),避免字段互相覆盖。
/// </summary>
public static IReadOnlyList<ShowcaseDifficultyCategory> GroupACategories { get; } = new List<ShowcaseDifficultyCategory>
{
new("DP_DM", "个人绳", "DP+DM(个人绳)"),
new("DA", "车轮跳", "DA(车轮跳)"),
new("DT_INTERACTION", "交互绳", "DT(交互绳)"),
new("DT_TRAVEL", "旅行跳", "DT(旅行跳)")
};
/// <summary>
/// B 组难度裁判要评的类别
/// </summary>
public static IReadOnlyList<ShowcaseDifficultyCategory> GroupBCategories { get; } = new List<ShowcaseDifficultyCategory>
{
new("DR", "个人绳", "DR(个人绳)"),
new("DB", "车轮跳", "DB(车轮跳)"),
new("DJ", "交互绳", "DJ(交互绳)"),
new("DL", "长绳", "DL(长绳)")
};
/// <summary>
/// 规定元素类别。成套动作中必须出现这 5 类,每缺一类参与 Q 扣减。
/// </summary>
public static IReadOnlyList<RequiredElementDefinition> RequiredElements { get; } = new List<RequiredElementDefinition>
{
new("REQ_SINGLE_ROPE", "个人绳"),
new("REQ_WHEEL", "车轮跳"),
new("REQ_WIRE", "交互绳"),
new("REQ_LONG_ROPE", "长绳"),
new("REQ_TRAVEL", "旅行跳")
};
/// <summary>
/// 表演赛裁判席位配置:A/B 组难度裁判各 3 人、表现裁判 3 人、主裁 1 人
/// </summary>
public const string JudgeRoleSchemaJson = """
[{"title":"难度裁判","slots":[{"code":"DGA","name":"难度裁判-A组裁判","min":3},{"code":"DGB","name":"难度裁判-B组裁判","min":3}]},{"title":"表现裁判","slots":[{"code":"P","name":"表现裁判-P","min":3}]},{"title":"主裁判","slots":[{"code":"T","name":"主裁判-T","min":1,"max":1}]}]
""";
/// <summary>
/// 表演赛计分参数配置。
/// totalMode=factor 表示按 R = D × (1 + P) × Q × M 相乘;A 组、B 组各自取平均后 difficultyDivisor=2 对应 D=(A+B)/2;
/// 主裁同时承担 Q(规定元素缺失数)与 M(扣分合计)两个因子,因此配置了两条 T。
/// </summary>
public const string RuleConfigJson = """
{"ruleType":"SHOWCASE","totalMode":"factor","baseScore":0,"scoreDecimals":2,"difficultyDivisor":2,"correctionNeedApproval":false,"roleAggregations":[{"roleType":"DGA","symbol":"A","label":"A组难度","sourceField":"DifficultyScore","targetField":"DifficultyScore","aggregate":"avg","sign":1,"factor":"D","factorWeight":1},{"roleType":"DGB","symbol":"B","label":"B组难度","sourceField":"DifficultyScore","targetField":"DifficultyScore","aggregate":"avg","sign":1,"factor":"D","factorWeight":1},{"roleType":"P","symbol":"P","label":"表现","sourceField":"PerformanceScore","targetField":"PerformanceScore","aggregate":"avg","sign":1,"factor":"P","factorWeight":0.05},{"roleType":"T","symbol":"Q","label":"规定元素缺失","sourceField":"RequiredMissingCount","targetField":"RequiredScore","aggregate":"max","sign":1,"factor":"Q","factorWeight":0.05},{"roleType":"T","symbol":"M","label":"扣分","sourceField":"DeductionScore","targetField":"DeductionScore","aggregate":"sum","sign":-1,"factor":"M","factorWeight":1}],"tieBreakers":["TotalScore","-DeductionScore","RequiredScore","PerformanceScore","DifficultyScore"]}
""";
/// <summary>
/// 表演赛全部评分项
/// </summary>
public static IReadOnlyList<PrescribedRuleItemDefinition> Items { get; } = BuildItems();
/// <summary>
/// 构造表演赛评分项:A/B 组各 4 个类别的难度等级、表现五维与失误、主裁扣分与 5 类规定元素、取消资格原因
/// </summary>
private static List<PrescribedRuleItemDefinition> BuildItems()
{
var items = new List<PrescribedRuleItemDefinition>();
var sort = 0;
// 一名难度裁判要按类别分块评分,因此每个类别的难度等级项都用类别编码作前缀独立成项。
foreach (var category in GroupACategories)
{
items.AddRange(FreestyleRuleDefinition.BuildDifficultyItems(RoleDifficultyGroupA,
category.Code, category.DisplayName, null, null, category.Code, ref sort));
}
foreach (var category in GroupBCategories)
{
items.AddRange(FreestyleRuleDefinition.BuildDifficultyItems(RoleDifficultyGroupB,
category.Code, category.DisplayName, null, null, category.Code, ref sort));
}
items.AddRange(FreestyleRuleDefinition.BuildPerformanceItems(ref sort));
// 表演赛主裁只记失误与违例,不记间歇、空间违例。
items.AddRange(FreestyleRuleDefinition.BuildChiefDeductionItems(null, null, ref sort));
// 规定元素:5 类中每类只要有动作即计一类完成,缺失类别数由服务端算出后参与 Q。
foreach (var element in RequiredElements)
{
items.Add(new PrescribedRuleItemDefinition
{
JudgeRoleType = RoleChief, ItemCode = element.ItemCode, ItemName = element.ItemName,
ItemType = ScoreItemType.RequiredElement, ScoreDimension = ScoreDimensionType.Required,
CalcMode = ScoreCalcMode.CountByUnit,
UnitValue = 1m, MinValue = 0m, MaxValue = 50m, StepValue = 1m, DefaultValue = 0m,
IsRequired = false, Sort = ++sort,
ExtensionJson = FreestyleItemExtension.RequiredElement()
});
}
items.Add(new PrescribedRuleItemDefinition
{
JudgeRoleType = RoleChief, ItemCode = "REQUIRED_MISSING", ItemName = "规定元素缺失(个)",
ItemType = ScoreItemType.RequiredElement, ScoreDimension = ScoreDimensionType.None,
CalcMode = ScoreCalcMode.DirectValue,
MinValue = 0m, MaxValue = RequiredTarget, StepValue = 1m, DefaultValue = 0m,
IsRequired = false, Sort = ++sort,
ExtensionJson = FreestyleItemExtension.RequiredMissing(RequiredTarget)
});
items.AddRange(FreestyleRuleDefinition.BuildDisqualifyItems(ref sort));
return items;
}
}
/// <summary>
/// 表演赛一个难度类别的定义
/// </summary>
/// <param name="Code">类别编码,同时作为评分项编码前缀与评分对象编码</param>
/// <param name="Name">类别名称</param>
/// <param name="DisplayName">页面分块标题,与裁判端原型逐字一致</param>
public record ShowcaseDifficultyCategory(string Code, string Name, string DisplayName);