585 lines
29 KiB
C#
585 lines
29 KiB
C#
namespace YD_Event.Application.JudgeScore;
|
||
|
||
/// <summary>
|
||
/// 花样赛内置计分规则定义(车轮跳花样、交互绳花样)。
|
||
/// <para>
|
||
/// 只在初始化/同步规则库时使用;运行期的评分项一律从 ScoreRule / ScoreRuleItem 读取,
|
||
/// 后台调整评分项、难度分值、席位配置后无需改代码。
|
||
/// </para>
|
||
/// <para>
|
||
/// 计分口径(裁判端原型 + 花样跳绳裁判端逻辑与测试验收说明书):<br/>
|
||
/// 车轮跳花样:DA×2、DB×2、P×2、T×1;dA=(da1+da2)/2、dB=(db1+db2)/2、D=(dA+dB)/2;<br/>
|
||
/// P=average(p1,p2)×0.05;Q=max(0,1-0.05×规定元素缺失类别数);M=max(0,1-(失误+违例+间歇));<br/>
|
||
/// 总分 R=max(0, D×(1+P)×Q×M)。<br/>
|
||
/// 交互绳花样:DJ、DT、P 各可配置 1~2 名裁判,T 固定 1 名;DJ 按全部动作取均值、DT 取最高 20 个动作(分母固定 20);<br/>
|
||
/// dJ=DJ 裁判分数平均值、dT=DT 裁判分数平均值、D=dJ×1.4+dT;P=P 裁判分数平均值×0.033;<br/>
|
||
/// M=max(0,1-(失误+违例+空间违例));总分 R=max(0, D×(1+P)×M),不计 Q。
|
||
/// </para>
|
||
/// </summary>
|
||
public static class FreestyleRuleDefinition
|
||
{
|
||
/// <summary>
|
||
/// 车轮跳花样规则编码
|
||
/// </summary>
|
||
public const string WheelRuleCode = "ROPE_FREESTYLE_WHEEL";
|
||
|
||
/// <summary>
|
||
/// 车轮跳花样规则名称
|
||
/// </summary>
|
||
public const string WheelRuleName = "车轮跳花样计分规则";
|
||
|
||
/// <summary>
|
||
/// 交互绳花样规则编码
|
||
/// </summary>
|
||
public const string WireRuleCode = "ROPE_FREESTYLE_WIRE";
|
||
|
||
/// <summary>
|
||
/// 交互绳花样规则名称
|
||
/// </summary>
|
||
public const string WireRuleName = "交互绳花样计分规则";
|
||
|
||
/// <summary>
|
||
/// 运动员A难度裁判角色类别编码(车轮跳花样)
|
||
/// </summary>
|
||
public const string RoleDifficultyA = "DA";
|
||
|
||
/// <summary>
|
||
/// 运动员B难度裁判角色类别编码(车轮跳花样)
|
||
/// </summary>
|
||
public const string RoleDifficultyB = "DB";
|
||
|
||
/// <summary>
|
||
/// 跳绳运动员难度裁判角色类别编码(交互绳花样,按全部动作取均值)
|
||
/// </summary>
|
||
public const string RoleDifficultyJumper = "DJ";
|
||
|
||
/// <summary>
|
||
/// 摇绳运动员难度裁判角色类别编码(交互绳花样,取最高 20 个动作)
|
||
/// </summary>
|
||
public const string RoleDifficultyTurner = "DT";
|
||
|
||
/// <summary>
|
||
/// 表现裁判角色类别编码
|
||
/// </summary>
|
||
public const string RolePerformance = "P";
|
||
|
||
/// <summary>
|
||
/// 主裁判角色类别编码
|
||
/// </summary>
|
||
public const string RoleChief = "T";
|
||
|
||
/// <summary>
|
||
/// 评分对象:运动员A(车轮跳花样)
|
||
/// </summary>
|
||
public const string SubjectA = "A";
|
||
|
||
/// <summary>
|
||
/// 评分对象:运动员B(车轮跳花样)
|
||
/// </summary>
|
||
public const string SubjectB = "B";
|
||
|
||
/// <summary>
|
||
/// 表现维度的基准分。人工加减与失误自动减都在基准分上调整。
|
||
/// </summary>
|
||
public const decimal PerformanceBaseValue = 12m;
|
||
|
||
/// <summary>
|
||
/// 表现维度分的上限(下限为 0)
|
||
/// </summary>
|
||
public const decimal PerformanceMaxValue = 24m;
|
||
|
||
/// <summary>
|
||
/// 车轮跳花样、精英个人花样的表现因子系数:P = average(p1,p2) × 0.05
|
||
/// </summary>
|
||
public const decimal PerformanceFactorWeight = 0.05m;
|
||
|
||
/// <summary>
|
||
/// 交互绳花样的表现因子系数:P = average(p1,p2) × 0.033
|
||
/// </summary>
|
||
public const decimal WirePerformanceFactorWeight = 0.033m;
|
||
|
||
/// <summary>
|
||
/// 每缺 1 类规定元素的 Q 扣减系数(花样跳绳打分.xmind 明确规定 Fq=2.5%=0.025):Q = max(0, 1 - 0.025 × 缺失类别数)
|
||
/// </summary>
|
||
public const decimal RequiredMissingUnit = 0.025m;
|
||
|
||
/// <summary>
|
||
/// 车轮跳花样每类规定元素的要求完成数量。
|
||
/// </summary>
|
||
public const decimal WheelRequiredCountPerElement = 4m;
|
||
|
||
/// <summary>
|
||
/// 车轮跳花样规定元素应完成总数:4 类 × 每类 4 个。
|
||
/// </summary>
|
||
public static decimal WheelRequiredTarget => WheelRequiredElements.Count * WheelRequiredCountPerElement;
|
||
|
||
/// <summary>
|
||
/// 摇绳运动员难度取用的动作数(少于 20 个动作时分母仍按 20)
|
||
/// </summary>
|
||
public const int TurnerTopCount = 20;
|
||
|
||
/// <summary>
|
||
/// 违例、间歇、空间违例的每次扣减值
|
||
/// </summary>
|
||
public const decimal ViolationUnitDeduction = 0.05m;
|
||
|
||
/// <summary>
|
||
/// 失误的递增扣减序列:第 1 次 0.05、第 2 次 0.075、第 3 次起每次 0.1
|
||
/// </summary>
|
||
public static readonly decimal[] FailureStepDeductions = { 0.05m, 0.075m, 0.1m };
|
||
|
||
/// <summary>
|
||
/// 难度等级分值表。必须查表取值,不得用浮点公式推导。
|
||
/// </summary>
|
||
public static IReadOnlyList<DifficultyLevelDefinition> DifficultyLevels { get; } = new List<DifficultyLevelDefinition>
|
||
{
|
||
new("DIFF_LEVEL_0", "0 级", 0m),
|
||
new("DIFF_LEVEL_0_5", "0.5 级", 0.12m),
|
||
new("DIFF_LEVEL_1", "1 级", 0.15m),
|
||
new("DIFF_LEVEL_2", "2 级", 0.23m),
|
||
new("DIFF_LEVEL_3", "3 级", 0.34m),
|
||
new("DIFF_LEVEL_4", "4 级", 0.51m),
|
||
new("DIFF_LEVEL_5", "5 级", 0.76m),
|
||
new("DIFF_LEVEL_6", "6 级", 1.14m),
|
||
new("DIFF_LEVEL_7", "7 级", 1.71m),
|
||
new("DIFF_LEVEL_8", "8 级", 2.56m)
|
||
};
|
||
|
||
/// <summary>
|
||
/// 交互绳跳绳运动员难度等级。间歇单独按0级记录;1~5级均支持减号、标准和加号档。
|
||
/// 减号按 x-0.25、加号按 x+0.5 换算,分值为 L(x)=1.5^x,保留4位小数。
|
||
/// </summary>
|
||
public static IReadOnlyList<DifficultyLevelDefinition> WireJumperDifficultyLevels { get; } =
|
||
BuildWireDifficultyLevels(includeBreak: true);
|
||
|
||
/// <summary>
|
||
/// 交互绳摇绳运动员难度等级。1~5级均支持减号、标准和加号档。
|
||
/// </summary>
|
||
public static IReadOnlyList<DifficultyLevelDefinition> WireTurnerDifficultyLevels { get; } =
|
||
BuildWireDifficultyLevels(includeBreak: false);
|
||
|
||
/// <summary>
|
||
/// 表现五维。权重合计必须为 1,页面按此顺序展示。
|
||
/// </summary>
|
||
public static IReadOnlyList<PerformanceDimensionDefinition> PerformanceDimensions { get; } = new List<PerformanceDimensionDefinition>
|
||
{
|
||
new("MUSIC", "音乐", 0.20m),
|
||
new("QUALITY", "完成质量", 0.25m),
|
||
new("ENTERTAINMENT", "娱乐", 0.25m),
|
||
new("CREATIVITY", "创意", 0.15m),
|
||
new("VARIETY", "多样性", 0.15m)
|
||
};
|
||
|
||
/// <summary>
|
||
/// 取消比赛资格原因,与裁判端原型逐字一致
|
||
/// </summary>
|
||
public static IReadOnlyList<DisqualifyReasonDefinition> DisqualifyReasons { get; } = new List<DisqualifyReasonDefinition>
|
||
{
|
||
new("DQ_DISCIPLINE", "违反赛场纪律"),
|
||
new("DQ_MEMBER_COUNT", "参赛人员不符合规定(不足或超过)"),
|
||
new("DQ_NO_ROPE_SHOES", "未穿跳绳鞋"),
|
||
new("DQ_EXTERNAL_AID", "使用外部助力器材"),
|
||
new("DQ_UNCIVILIZED", "比赛中嚼口香糖/不文明用语等"),
|
||
new("DQ_ANTI_OLYMPIC", "有悖奥林匹克精神的元素(描绘暴力、战争、宗教、色情有关的元素)"),
|
||
new("DQ_LATE_60S", "运动员(队)被叫超过60秒未上场"),
|
||
new("DQ_TATTOO", "显露纹身")
|
||
};
|
||
|
||
/// <summary>
|
||
/// 车轮跳花样的规定元素类别,只要该类别有动作即计一类完成
|
||
/// </summary>
|
||
public static IReadOnlyList<RequiredElementDefinition> WheelRequiredElements { get; } = new List<RequiredElementDefinition>
|
||
{
|
||
new("REQ_STRENGTH_GYMNASTICS", "力量和体操"),
|
||
new("REQ_MULTIPLE_ROPE", "多摇"),
|
||
new("REQ_ROPE_MANIPULATION", "绳子运用"),
|
||
new("REQ_INTERACTION", "互动配合")
|
||
};
|
||
|
||
/// <summary>
|
||
/// 个人花样/同步花样裁判席位配置:DP/DM/DR 难度裁判各 1~2 人、表现裁判 1~2 人、主裁 1 人
|
||
/// </summary>
|
||
public const string SingleJudgeRoleSchemaJson = """
|
||
[{"title":"主裁判","slots":[{"code":"T","name":"主裁判-T","min":1,"max":1}]},{"title":"难度裁判","slots":[{"code":"DP","name":"难度裁判-DP(力量体操裁判)","min":1,"max":2},{"code":"DM","name":"难度裁判-DM(多摇裁判)","min":1,"max":2},{"code":"DR","name":"难度裁判-DR(绳子运用裁判)","min":1,"max":2}]},{"title":"表现裁判","slots":[{"code":"P","name":"表现裁判-P","min":1,"max":2}]}]
|
||
""";
|
||
|
||
/// <summary>
|
||
/// 车轮跳花样裁判席位配置:运动员A/B难度裁判各 1~2 人、表现裁判 1~2 人、主裁 1 人
|
||
/// </summary>
|
||
public const string WheelJudgeRoleSchemaJson = """
|
||
[{"title":"主裁判","slots":[{"code":"T","name":"主裁判-T","min":1,"max":1}]},{"title":"难度裁判","slots":[{"code":"DA","name":"难度裁判-DA(运动员A难度裁判)","min":1,"max":2},{"code":"DB","name":"难度裁判-DB(运动员B难度裁判)","min":1,"max":2}]},{"title":"表现裁判","slots":[{"code":"P","name":"表现裁判-P","min":1,"max":2}]}]
|
||
""";
|
||
|
||
/// <summary>
|
||
/// 交互绳花样裁判席位配置:跳绳者/摇绳者难度裁判各 1~2 人、表现裁判 1~2 人、主裁 1 人
|
||
/// </summary>
|
||
public const string WireJudgeRoleSchemaJson = """
|
||
[{"title":"主裁判","slots":[{"code":"T","name":"主裁判-T","min":1,"max":1}]},{"title":"难度裁判","slots":[{"code":"DJ","name":"难度裁判-DJ(跳绳运动员难度裁判)","min":1,"max":2},{"code":"DT","name":"难度裁判-DT(摇绳运动员难度裁判)","min":1,"max":2}]},{"title":"表现裁判","slots":[{"code":"P","name":"表现裁判-P","min":1,"max":2}]}]
|
||
""";
|
||
|
||
/// <summary>
|
||
/// 车轮跳花样计分参数配置。
|
||
/// totalMode=factor 表示按 R = D × (1 + P) × Q × M 相乘;difficultyDivisor=2 对应 D=(dA+dB)/2;
|
||
/// 主裁同时承担 Q(规定元素缺失数)与 M(扣分合计)两个因子,因此配置了两条 T。
|
||
/// </summary>
|
||
public const string WheelRuleConfigJson = """
|
||
{"ruleType":"FREESTYLE_WHEEL","totalMode":"factor","baseScore":0,"scoreDecimals":2,"difficultyDivisor":2,"correctionNeedApproval":false,"roleAggregations":[{"roleType":"DA","symbol":"dA","label":"运动员A难度","sourceField":"DifficultyScore","targetField":"DifficultyScore","aggregate":"avg","sign":1,"factor":"D","factorWeight":1},{"roleType":"DB","symbol":"dB","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.025},{"roleType":"T","symbol":"M","label":"扣分","sourceField":"DeductionScore","targetField":"DeductionScore","aggregate":"sum","sign":-1,"factor":"M","factorWeight":1}],"tieBreakers":["TotalScore","-DeductionScore","RequiredScore","PerformanceScore","DifficultyScore"]}
|
||
""";
|
||
|
||
/// <summary>
|
||
/// 交互绳花样计分参数配置。
|
||
/// D = dJ × 1.4 + dT 是加权合并而非取平均,因此 difficultyDivisor 配置为 1;
|
||
/// 交互绳总分无 Q 因子,主裁只配置 M。
|
||
/// </summary>
|
||
public const string WireRuleConfigJson = """
|
||
{"ruleType":"FREESTYLE_WIRE","totalMode":"factor","baseScore":0,"scoreDecimals":2,"difficultyDivisor":1,"correctionNeedApproval":false,"roleAggregations":[{"roleType":"DJ","symbol":"dJ","label":"跳绳运动员难度","sourceField":"DifficultyScore","targetField":"DifficultyScore","aggregate":"avg","sign":1,"factor":"D","factorWeight":1.4},{"roleType":"DT","symbol":"dT","label":"摇绳运动员难度","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.033},{"roleType":"T","symbol":"M","label":"扣分","sourceField":"DeductionScore","targetField":"DeductionScore","aggregate":"sum","sign":-1,"factor":"M","factorWeight":1}],"tieBreakers":["TotalScore","-DeductionScore","PerformanceScore","DifficultyScore"]}
|
||
""";
|
||
|
||
/// <summary>
|
||
/// 车轮跳花样全部评分项
|
||
/// </summary>
|
||
public static IReadOnlyList<PrescribedRuleItemDefinition> WheelItems { get; } = BuildWheelItems();
|
||
|
||
/// <summary>
|
||
/// 交互绳花样全部评分项
|
||
/// </summary>
|
||
public static IReadOnlyList<PrescribedRuleItemDefinition> WireItems { get; } = BuildWireItems();
|
||
|
||
/// <summary>
|
||
/// 构造车轮跳花样评分项:DA/DB 难度等级、表现五维与失误、主裁扣分与规定元素、取消资格原因
|
||
/// </summary>
|
||
private static List<PrescribedRuleItemDefinition> BuildWheelItems()
|
||
{
|
||
var items = new List<PrescribedRuleItemDefinition>();
|
||
var sort = 0;
|
||
|
||
// 运动员A、B 的难度裁判各自只看一名运动员,难度明细带 subjectCode 便于后端聚合与定位。
|
||
items.AddRange(BuildDifficultyItems(RoleDifficultyA, SubjectA, null, null, null, null, ref sort));
|
||
items.AddRange(BuildDifficultyItems(RoleDifficultyB, SubjectB, null, null, null, null, ref sort));
|
||
items.AddRange(BuildPerformanceItems(ref sort));
|
||
items.AddRange(BuildChiefDeductionItems("T_INTERVAL", "间歇", ref sort));
|
||
|
||
// 规定元素:4 类元素每类均需完成 4 个,未完成数由服务端算出后参与 Q。
|
||
foreach (var element in WheelRequiredElements)
|
||
{
|
||
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 = WheelRequiredCountPerElement, StepValue = 1m, DefaultValue = 0m,
|
||
IsRequired = false, Sort = ++sort,
|
||
ExtensionJson = FreestyleItemExtension.Count("个")
|
||
});
|
||
}
|
||
items.Add(new PrescribedRuleItemDefinition
|
||
{
|
||
JudgeRoleType = RoleChief, ItemCode = "REQUIRED_MISSING", ItemName = "规定元素缺失(个)",
|
||
ItemType = ScoreItemType.RequiredElement, ScoreDimension = ScoreDimensionType.None,
|
||
CalcMode = ScoreCalcMode.DirectValue,
|
||
MinValue = 0m, MaxValue = WheelRequiredTarget, StepValue = 1m, DefaultValue = 0m,
|
||
IsRequired = false, Sort = ++sort,
|
||
ExtensionJson = FreestyleItemExtension.RequiredMissing(WheelRequiredTarget)
|
||
});
|
||
|
||
items.AddRange(BuildDisqualifyItems(ref sort));
|
||
return items;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 构造交互绳花样评分项:DJ 按全部动作取均值、DT 取最高 20 个动作,表现与主裁扣分同车轮跳但不计规定元素
|
||
/// </summary>
|
||
private static List<PrescribedRuleItemDefinition> BuildWireItems()
|
||
{
|
||
var items = new List<PrescribedRuleItemDefinition>();
|
||
var sort = 0;
|
||
|
||
items.AddRange(BuildDifficultyItems(RoleDifficultyJumper, null, null,
|
||
Scoring.Dtos.DifficultyAggregateMode.Average, null, null, ref sort, WireJumperDifficultyLevels));
|
||
items.AddRange(BuildDifficultyItems(RoleDifficultyTurner, null, null,
|
||
Scoring.Dtos.DifficultyAggregateMode.Top, TurnerTopCount, null, ref sort, WireTurnerDifficultyLevels));
|
||
items.AddRange(BuildPerformanceItems(ref sort));
|
||
items.AddRange(BuildChiefDeductionItems("T_SPACE_VIOLATION", "空间违例", ref sort));
|
||
items.AddRange(BuildDisqualifyItems(ref sort));
|
||
return items;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 构造一个难度裁判类型的等级计数项。
|
||
/// aggregate 为空表示按数量 × 等级分值直接累加;avg 按动作总数取均值;top 取最高 topCount 个动作。
|
||
/// itemCodePrefix 用于表演赛把同一裁判的多个类别拆成独立评分项(如 SR_DIFF_LEVEL_3),避免编码互相覆盖。
|
||
/// </summary>
|
||
internal static List<PrescribedRuleItemDefinition> BuildDifficultyItems(string roleType, string? subjectCode,
|
||
string? subjectName, string? aggregate, int? topCount, string? itemCodePrefix, ref int sort,
|
||
IReadOnlyList<DifficultyLevelDefinition>? difficultyLevels = null)
|
||
{
|
||
var items = new List<PrescribedRuleItemDefinition>();
|
||
foreach (var level in difficultyLevels ?? DifficultyLevels)
|
||
{
|
||
items.Add(new PrescribedRuleItemDefinition
|
||
{
|
||
JudgeRoleType = roleType,
|
||
ItemCode = string.IsNullOrEmpty(itemCodePrefix) ? level.ItemCode : $"{itemCodePrefix}_{level.ItemCode}",
|
||
ItemName = level.LevelName,
|
||
ItemType = ScoreItemType.DifficultyCount, ScoreDimension = ScoreDimensionType.Difficulty,
|
||
CalcMode = ScoreCalcMode.CountByUnit,
|
||
UnitValue = level.LevelValue, MinValue = 0m, MaxValue = 50m, StepValue = 1m, DefaultValue = 0m,
|
||
IsRequired = false, Sort = ++sort,
|
||
ExtensionJson = FreestyleItemExtension.Difficulty(subjectCode, subjectName, aggregate, topCount)
|
||
});
|
||
}
|
||
return items;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 构造表现裁判评分项:五维各一组人工加/减项 + 一个只读维度分自动项,再加一个失误次数项。
|
||
/// 失误联动五维自动减分由服务端在提交时重算,客户端只能改失误次数。
|
||
/// </summary>
|
||
internal static List<PrescribedRuleItemDefinition> BuildPerformanceItems(ref int sort)
|
||
{
|
||
var items = new List<PrescribedRuleItemDefinition>();
|
||
foreach (var dimension in PerformanceDimensions)
|
||
{
|
||
items.Add(new PrescribedRuleItemDefinition
|
||
{
|
||
JudgeRoleType = RolePerformance, ItemCode = $"{dimension.Code}_PLUS", ItemName = $"{dimension.Name}(加数量)",
|
||
ItemType = ScoreItemType.Performance, ScoreDimension = ScoreDimensionType.Performance,
|
||
CalcMode = ScoreCalcMode.CountByUnit,
|
||
UnitValue = 1m, MinValue = 0m, MaxValue = 50m, StepValue = 1m, DefaultValue = 0m,
|
||
IsRequired = false, Sort = ++sort,
|
||
ExtensionJson = FreestyleItemExtension.PerformanceAdjust(dimension.Code, 1)
|
||
});
|
||
items.Add(new PrescribedRuleItemDefinition
|
||
{
|
||
JudgeRoleType = RolePerformance, ItemCode = $"{dimension.Code}_MINUS", ItemName = $"{dimension.Name}(减数量)",
|
||
ItemType = ScoreItemType.Performance, ScoreDimension = ScoreDimensionType.Performance,
|
||
CalcMode = ScoreCalcMode.CountByUnit,
|
||
UnitValue = 1m, MinValue = 0m, MaxValue = 50m, StepValue = 1m, DefaultValue = 0m,
|
||
IsRequired = false, Sort = ++sort,
|
||
ExtensionJson = FreestyleItemExtension.PerformanceAdjust(dimension.Code, -1)
|
||
});
|
||
items.Add(new PrescribedRuleItemDefinition
|
||
{
|
||
JudgeRoleType = RolePerformance, ItemCode = $"{dimension.Code}_SCORE", ItemName = dimension.Name,
|
||
ItemType = ScoreItemType.Performance, ScoreDimension = ScoreDimensionType.Performance,
|
||
CalcMode = ScoreCalcMode.DirectValue,
|
||
UnitValue = 1m, MinValue = 0m, MaxValue = PerformanceMaxValue, StepValue = 1m,
|
||
DefaultValue = PerformanceBaseValue, IsRequired = false, Sort = ++sort,
|
||
ExtensionJson = FreestyleItemExtension.PerformanceDimension(dimension.Code, dimension.Weight)
|
||
});
|
||
}
|
||
items.Add(new PrescribedRuleItemDefinition
|
||
{
|
||
JudgeRoleType = RolePerformance, ItemCode = "FAILURE_COUNT", ItemName = "失误次数",
|
||
ItemType = ScoreItemType.Performance, ScoreDimension = ScoreDimensionType.None,
|
||
CalcMode = ScoreCalcMode.CountByUnit,
|
||
UnitValue = 1m, MinValue = 0m, MaxValue = 50m, StepValue = 1m, DefaultValue = 0m,
|
||
IsRequired = false, Sort = ++sort,
|
||
ExtensionJson = FreestyleItemExtension.FailureCount()
|
||
});
|
||
return items;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 构造主裁扣分项。失误按递增序列扣减,违例与 extraCode 指定的第三项(车轮跳为间歇、交互绳为空间违例)按每次 0.05 扣减。
|
||
/// extraCode 为空时只生成失误与违例两项(表演赛)。
|
||
/// </summary>
|
||
internal static List<PrescribedRuleItemDefinition> BuildChiefDeductionItems(string? extraCode, string? extraName, ref int sort)
|
||
{
|
||
var items = new List<PrescribedRuleItemDefinition>
|
||
{
|
||
new()
|
||
{
|
||
JudgeRoleType = RoleChief, ItemCode = "T_VIOLATION", ItemName = "违例",
|
||
ItemType = ScoreItemType.Deduction, ScoreDimension = ScoreDimensionType.Deduction,
|
||
CalcMode = ScoreCalcMode.CountByUnit,
|
||
UnitValue = ViolationUnitDeduction, MinValue = 0m, MaxValue = 50m, StepValue = 1m, DefaultValue = 0m,
|
||
IsRequired = false, Sort = ++sort,
|
||
ExtensionJson = FreestyleItemExtension.Count("次")
|
||
}
|
||
};
|
||
if (!string.IsNullOrEmpty(extraCode))
|
||
{
|
||
items.Add(new PrescribedRuleItemDefinition
|
||
{
|
||
JudgeRoleType = RoleChief, ItemCode = extraCode, ItemName = extraName ?? extraCode,
|
||
ItemType = ScoreItemType.Deduction, ScoreDimension = ScoreDimensionType.Deduction,
|
||
CalcMode = ScoreCalcMode.CountByUnit,
|
||
UnitValue = ViolationUnitDeduction, MinValue = 0m, MaxValue = 50m, StepValue = 1m, DefaultValue = 0m,
|
||
IsRequired = false, Sort = ++sort,
|
||
ExtensionJson = FreestyleItemExtension.Count("次")
|
||
});
|
||
}
|
||
items.Add(new PrescribedRuleItemDefinition
|
||
{
|
||
JudgeRoleType = RoleChief, ItemCode = "T_FAILURE", ItemName = "失误",
|
||
ItemType = ScoreItemType.Deduction, ScoreDimension = ScoreDimensionType.Deduction,
|
||
CalcMode = ScoreCalcMode.CountByUnit,
|
||
UnitValue = FailureStepDeductions[0], MinValue = 0m, MaxValue = 50m, StepValue = 1m, DefaultValue = 0m,
|
||
IsRequired = false, Sort = ++sort,
|
||
ExtensionJson = FreestyleItemExtension.StepDeduction(FailureStepDeductions)
|
||
});
|
||
return items;
|
||
}
|
||
|
||
private static List<DifficultyLevelDefinition> BuildWireDifficultyLevels(bool includeBreak)
|
||
{
|
||
var levels = new List<DifficultyLevelDefinition>();
|
||
if (includeBreak)
|
||
levels.Add(new DifficultyLevelDefinition("DIFF_LEVEL_0", "0 级(间歇)", 0m));
|
||
|
||
for (var level = 1; level <= 5; level++)
|
||
{
|
||
levels.Add(NewWireDifficultyLevel(level, "MINUS", "-", -0.25));
|
||
levels.Add(NewWireDifficultyLevel(level, null, string.Empty, 0));
|
||
levels.Add(NewWireDifficultyLevel(level, "PLUS", "+", 0.5));
|
||
}
|
||
return levels;
|
||
}
|
||
|
||
private static DifficultyLevelDefinition NewWireDifficultyLevel(int level, string? codeSuffix,
|
||
string nameSuffix, double levelOffset)
|
||
{
|
||
var itemCode = codeSuffix == null ? $"DIFF_LEVEL_{level}" : $"DIFF_LEVEL_{level}_{codeSuffix}";
|
||
var levelValue = decimal.Round((decimal)Math.Pow(1.5, level + levelOffset), 4,
|
||
MidpointRounding.AwayFromZero);
|
||
return new DifficultyLevelDefinition(itemCode, $"{level}{nameSuffix} 级", levelValue);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 构造主裁的取消比赛资格原因项,勾选任一项即整条成绩按 0 分处理
|
||
/// </summary>
|
||
internal static List<PrescribedRuleItemDefinition> BuildDisqualifyItems(ref int sort)
|
||
{
|
||
var items = new List<PrescribedRuleItemDefinition>();
|
||
foreach (var reason in DisqualifyReasons)
|
||
{
|
||
items.Add(new PrescribedRuleItemDefinition
|
||
{
|
||
JudgeRoleType = RoleChief, ItemCode = reason.ItemCode, ItemName = reason.ReasonName,
|
||
ItemType = ScoreItemType.Disqualify, ScoreDimension = ScoreDimensionType.None,
|
||
CalcMode = ScoreCalcMode.RecordOnly,
|
||
IsRequired = false, Sort = ++sort
|
||
});
|
||
}
|
||
return items;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 一个难度等级的定义
|
||
/// </summary>
|
||
/// <param name="ItemCode">评分项编码</param>
|
||
/// <param name="LevelName">等级名称</param>
|
||
/// <param name="LevelValue">等级分值</param>
|
||
public record DifficultyLevelDefinition(string ItemCode, string LevelName, decimal LevelValue);
|
||
|
||
/// <summary>
|
||
/// 一个表现维度的定义
|
||
/// </summary>
|
||
/// <param name="Code">维度编码</param>
|
||
/// <param name="Name">维度名称</param>
|
||
/// <param name="Weight">在单裁判加权分里的权重</param>
|
||
public record PerformanceDimensionDefinition(string Code, string Name, decimal Weight);
|
||
|
||
/// <summary>
|
||
/// 一条取消比赛资格原因的定义
|
||
/// </summary>
|
||
/// <param name="ItemCode">评分项编码</param>
|
||
/// <param name="ReasonName">原因名称</param>
|
||
public record DisqualifyReasonDefinition(string ItemCode, string ReasonName);
|
||
|
||
/// <summary>
|
||
/// 一类规定元素的定义
|
||
/// </summary>
|
||
/// <param name="ItemCode">评分项编码</param>
|
||
/// <param name="ItemName">类别名称</param>
|
||
public record RequiredElementDefinition(string ItemCode, string ItemName);
|
||
|
||
/// <summary>
|
||
/// 花样赛评分项扩展配置(ScoreRuleItem.ExtensionJson)的构造。
|
||
/// 字段名与 ScoreSheetCalculationService.CreateDefinition 的解析逻辑一一对应。
|
||
/// </summary>
|
||
public static class FreestyleItemExtension
|
||
{
|
||
/// <summary>
|
||
/// 难度等级计数项:subjectCode/subjectName 区分同一裁判要评的多个对象(车轮跳的两名运动员、表演赛的多个器械类别),
|
||
/// difficultyAggregate 指定聚合方式
|
||
/// </summary>
|
||
public static string Difficulty(string? subjectCode, string? subjectName, string? aggregate, int? topCount)
|
||
{
|
||
var node = new JObject { ["inputMode"] = "count", ["unitLabel"] = "个" };
|
||
if (!string.IsNullOrEmpty(subjectCode)) node["subjectCode"] = subjectCode;
|
||
if (!string.IsNullOrEmpty(subjectName)) node["subjectName"] = subjectName;
|
||
if (!string.IsNullOrEmpty(aggregate)) node["difficultyAggregate"] = aggregate;
|
||
if (topCount is > 0) node["difficultyTopCount"] = topCount.Value;
|
||
return node.ToString(Formatting.None);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 表现维度的人工加/减项:adjustSign 为 1 表示每次计数使维度分 +1,-1 表示 -1
|
||
/// </summary>
|
||
public static string PerformanceAdjust(string dimensionCode, int adjustSign) =>
|
||
new JObject
|
||
{
|
||
["inputMode"] = "count", ["unitLabel"] = "次",
|
||
["dimensionCode"] = dimensionCode, ["adjustSign"] = adjustSign
|
||
}.ToString(Formatting.None);
|
||
|
||
/// <summary>
|
||
/// 表现维度分(只读自动项):基准 12 分,按权重参与单裁判加权分
|
||
/// </summary>
|
||
public static string PerformanceDimension(string dimensionCode, decimal weight) =>
|
||
new JObject
|
||
{
|
||
["inputMode"] = "score", ["isAuto"] = true,
|
||
["dimensionCode"] = dimensionCode, ["dimensionWeight"] = weight,
|
||
["baseValue"] = FreestyleRuleDefinition.PerformanceBaseValue
|
||
}.ToString(Formatting.None);
|
||
|
||
/// <summary>
|
||
/// 失误次数项:每次失误使五个维度的自动减分各 +1
|
||
/// </summary>
|
||
public static string FailureCount() =>
|
||
new JObject
|
||
{
|
||
["inputMode"] = "count", ["unitLabel"] = "次", ["isFailureCount"] = true,
|
||
["remark"] = "表现裁判记一次失误,其对应的表现五个方面(音乐,完成质量,创意,娱乐和多样性)减数量+1"
|
||
}.ToString(Formatting.None);
|
||
|
||
/// <summary>
|
||
/// 递增扣分项:第 1 次 0.05、第 2 次 0.075、第 3 次起每次 0.1
|
||
/// </summary>
|
||
public static string StepDeduction(IEnumerable<decimal> steps) =>
|
||
new JObject
|
||
{
|
||
["inputMode"] = "count", ["unitLabel"] = "次",
|
||
["stepDeductions"] = new JArray(steps.Cast<object>().ToArray()),
|
||
["remark"] = "第1次扣0.05,第2次扣0.075,第3次起每次扣0.1"
|
||
}.ToString(Formatting.None);
|
||
|
||
/// <summary>
|
||
/// 普通计数项
|
||
/// </summary>
|
||
public static string Count(string unitLabel) =>
|
||
new JObject { ["inputMode"] = "count", ["unitLabel"] = unitLabel }.ToString(Formatting.None);
|
||
|
||
/// <summary>
|
||
/// 规定元素类别项:录入数量大于 0 即计一类完成(表演赛使用)。
|
||
/// </summary>
|
||
public static string RequiredElement() =>
|
||
new JObject
|
||
{
|
||
["inputMode"] = "count", ["unitLabel"] = "个", ["requiredAsCategory"] = true
|
||
}.ToString(Formatting.None);
|
||
|
||
/// <summary>
|
||
/// 规定元素缺失数(只读自动项):缺失数 = max(0, 目标类别数 - 完成类别数)
|
||
/// </summary>
|
||
public static string RequiredMissing(decimal target) =>
|
||
new JObject
|
||
{
|
||
["inputMode"] = "score", ["isAuto"] = true, ["unitLabel"] = "个", ["requiredTarget"] = target
|
||
}.ToString(Formatting.None);
|
||
}
|