349 lines
14 KiB
C#
349 lines
14 KiB
C#
namespace YD_Event.Application.JudgeScore;
|
||
|
||
/// <summary>
|
||
/// 规定赛(花样跳绳规定套路)内置计分规则定义。
|
||
/// <para>
|
||
/// 只在初始化/同步规则库时使用;运行期的评分项一律从 ScoreRule / ScoreRuleItem 读取,
|
||
/// 后台调整评分项、分值上限、扣分单位后无需改代码。
|
||
/// </para>
|
||
/// <para>
|
||
/// 计分口径(原型 + 会议纪要):<br/>
|
||
/// T1 = 创意编排裁判(CJ)各自“创意编排分”的平均值,四舍五入保留两位小数;<br/>
|
||
/// T2 = 完成质量裁判(CQ)各自“完成质量分”的平均值,四舍五入保留两位小数;<br/>
|
||
/// T3 = 主裁(T)判定的违例项次 × 每项 5 分;<br/>
|
||
/// 规定赛总分 = T1 + T2 - T3。
|
||
/// </para>
|
||
/// </summary>
|
||
public static class PrescribedRuleDefinition
|
||
{
|
||
/// <summary>
|
||
/// 规定赛规则编码
|
||
/// </summary>
|
||
public const string RuleCode = "ROPE_PRESCRIBED";
|
||
|
||
/// <summary>
|
||
/// 规定赛规则名称
|
||
/// </summary>
|
||
public const string RuleName = "规定赛计分规则";
|
||
|
||
/// <summary>
|
||
/// 创意编排裁判角色类别编码
|
||
/// </summary>
|
||
public const string RoleChoreography = "CJ";
|
||
|
||
/// <summary>
|
||
/// 完成质量裁判角色类别编码
|
||
/// </summary>
|
||
public const string RoleCompletionQuality = "CQ";
|
||
|
||
/// <summary>
|
||
/// 主裁角色类别编码
|
||
/// </summary>
|
||
public const string RoleChief = "T";
|
||
|
||
/// <summary>
|
||
/// 每项违例的扣分值(主裁每判定一项违例扣 5 分)
|
||
/// </summary>
|
||
public const decimal ViolationUnitDeduction = 5m;
|
||
|
||
/// <summary>
|
||
/// 裁判席位配置。
|
||
/// CJ / CQ 不限人数(成绩按平均值计算,标准配置各 3 人写在 RuleConfigJson 里),主裁固定 1 人。
|
||
/// </summary>
|
||
public const string JudgeRoleSchemaJson = """
|
||
[{"title":"创意编排裁判","slots":[{"code":"CJ","name":"创意编排裁判","min":1}]},{"title":"完成质量裁判","slots":[{"code":"CQ","name":"完成质量裁判","min":1}]},{"title":"主裁","slots":[{"code":"T","name":"主裁","min":1,"max":1}]}]
|
||
""";
|
||
|
||
/// <summary>
|
||
/// 计分参数配置。
|
||
/// roles 描述每个裁判类别在打分页与聚合计算中的表现:
|
||
/// dimension 对应 ScoreRuleItem.ScoreDimension;aggregateMode=score 表示页面合计展示分值,count 表示展示项次数量;
|
||
/// standardCount 是标准配置人数(仅用于展示“已提交 x/3”),实际参与平均的人数以已提交评分单为准。
|
||
/// </summary>
|
||
public const string RuleConfigJson = """
|
||
{"ruleType":"PRESCRIBED","unitDeduction":5,"baseScore":100,"totalFormula":"T1+T2-T3","roles":[{"code":"CJ","name":"创意编排裁判","symbol":"T1","dimension":2,"scoreLabel":"创意编排分","valueLabel":"分数","aggregateMode":"score","standardCount":3,"canDisqualify":false},{"code":"CQ","name":"完成质量裁判","symbol":"T2","dimension":3,"scoreLabel":"完成质量分","valueLabel":"分数","aggregateMode":"score","standardCount":3,"canDisqualify":false},{"code":"T","name":"主裁","symbol":"T3","dimension":4,"scoreLabel":"扣分","valueLabel":"数量","aggregateMode":"count","standardCount":1,"canDisqualify":true,"tip":"每项每次扣5分"}]}
|
||
""";
|
||
|
||
/// <summary>
|
||
/// 规定赛全部评分项。名称与裁判端原型逐字一致,改动请同步原型。
|
||
/// </summary>
|
||
public static IReadOnlyList<PrescribedRuleItemDefinition> Items { get; } = BuildItems();
|
||
|
||
/// <summary>
|
||
/// 构造规定赛评分项清单
|
||
/// </summary>
|
||
private static List<PrescribedRuleItemDefinition> BuildItems()
|
||
{
|
||
var items = new List<PrescribedRuleItemDefinition>();
|
||
var sort = 0;
|
||
|
||
// 创意编排:两项各满分 10 分,先选整数分再用 ±0.5 微调,合计即 T1 的单裁判得分。
|
||
items.Add(new PrescribedRuleItemDefinition
|
||
{
|
||
JudgeRoleType = RoleChoreography, ItemCode = "CJ_TRANSITION",
|
||
ItemName = "过渡连接、成套创编、场地运用",
|
||
ItemType = ScoreItemType.Prescribed, ScoreDimension = ScoreDimensionType.Performance,
|
||
CalcMode = ScoreCalcMode.DirectValue,
|
||
MinValue = 0m, MaxValue = 10m, StepValue = 0.5m, DefaultValue = 0m, IsRequired = true, Sort = ++sort
|
||
});
|
||
items.Add(new PrescribedRuleItemDefinition
|
||
{
|
||
JudgeRoleType = RoleChoreography, ItemCode = "CJ_PERFORMANCE",
|
||
ItemName = "现场表现效果",
|
||
ItemType = ScoreItemType.Prescribed, ScoreDimension = ScoreDimensionType.Performance,
|
||
CalcMode = ScoreCalcMode.DirectValue,
|
||
MinValue = 0m, MaxValue = 10m, StepValue = 0.5m, DefaultValue = 0m, IsRequired = true, Sort = ++sort
|
||
});
|
||
|
||
// 完成质量:个人项目与集体项目的满分不同,按项目类型取上限;
|
||
// 只配置了集体满分的两项在个人项目里不出现(IndividualMax = null)。
|
||
items.Add(NewCompletionQualityItem("CQ_ACCURACY", "动作的准确性(个人30分/集体20分)", 30m, 20m, ++sort));
|
||
items.Add(NewCompletionQualityItem("CQ_PROFICIENCY", "动作的熟练性(个人20分/集体15分)", 20m, 15m, ++sort));
|
||
items.Add(NewCompletionQualityItem("CQ_SYNCHRONIZATION", "动作的合拍性(个人30分/集体15分)", 30m, 15m, ++sort));
|
||
items.Add(NewCompletionQualityItem("CQ_ABILITY_CONSISTENCY", "运动能力一致性(集体15分)", null, 15m, ++sort));
|
||
items.Add(NewCompletionQualityItem("CQ_SKILL_CONSISTENCY", "表演技巧一致性(集体15分)", null, 15m, ++sort));
|
||
|
||
// 主裁违例:录入的是“项次数量”,扣分 = 数量 × 5。
|
||
items.Add(NewViolationItem("T_NO_ROPE_SALUTE", "赛前或赛后未行绳礼(每队)", ++sort));
|
||
items.Add(NewViolationItem("T_APPEARANCE", "运动员的头发、着装、仪容不符合规定,佩戴饰物、饰物掉落", ++sort));
|
||
items.Add(NewViolationItem("T_CHANGE_1X8", "更改1×8拍规定元素内容", ++sort));
|
||
items.Add(NewViolationItem("T_INTERRUPT_4S", "套路中断4秒以上", ++sort));
|
||
items.Add(NewViolationItem("T_VENUE_FOUL", "场地犯规(踩线/出界)", ++sort));
|
||
|
||
// 主裁取消比赛资格原因:只登记不参与合计,勾选任一即整条成绩取消资格。
|
||
items.Add(NewDisqualifyItem("DQ_LATE_60S", "运动(队)被叫超过60秒未上场", ++sort));
|
||
items.Add(NewDisqualifyItem("DQ_MEMBER_COUNT", "参赛人员不符合规定(不足或超过)", ++sort));
|
||
items.Add(NewDisqualifyItem("DQ_NO_ROPE_SHOES", "未穿跳绳鞋", ++sort));
|
||
items.Add(NewDisqualifyItem("DQ_EXTERNAL_AID", "使用外部助力器材", ++sort));
|
||
items.Add(NewDisqualifyItem("DQ_UNCIVILIZED", "比赛中嚼口香糖/不文明用语等", ++sort));
|
||
items.Add(NewDisqualifyItem("DQ_ANTI_OLYMPIC", "有悖奥林匹克精神的元素(描绘暴力、战争、宗教、色情有关的元素)", ++sort));
|
||
items.Add(NewDisqualifyItem("DQ_CHANGE_4X8", "更改4×8拍规定元素内容", ++sort));
|
||
|
||
return items;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 构造一条完成质量评分项,个人/集体满分写进扩展配置,运行期按项目类型取值
|
||
/// </summary>
|
||
private static PrescribedRuleItemDefinition NewCompletionQualityItem(string itemCode, string itemName,
|
||
decimal? individualMax, decimal? teamMax, int sort) => new()
|
||
{
|
||
JudgeRoleType = RoleCompletionQuality, ItemCode = itemCode, ItemName = itemName,
|
||
ItemType = ScoreItemType.Prescribed, ScoreDimension = ScoreDimensionType.Required,
|
||
CalcMode = ScoreCalcMode.DirectValue,
|
||
MinValue = 0m, MaxValue = individualMax ?? teamMax, StepValue = 0.5m, DefaultValue = 0m,
|
||
IsRequired = true, Sort = sort,
|
||
ExtensionJson = ScoreItemExtension.Build(individualMax, teamMax)
|
||
};
|
||
|
||
/// <summary>
|
||
/// 构造一条主裁违例扣分项,录入数量后按每项 5 分累加
|
||
/// </summary>
|
||
private static PrescribedRuleItemDefinition NewViolationItem(string itemCode, string itemName, int sort) => new()
|
||
{
|
||
JudgeRoleType = RoleChief, ItemCode = itemCode, ItemName = itemName,
|
||
ItemType = ScoreItemType.Deduction, ScoreDimension = ScoreDimensionType.Deduction,
|
||
CalcMode = ScoreCalcMode.CountByUnit,
|
||
UnitValue = ViolationUnitDeduction, MinValue = 0m, MaxValue = 20m, StepValue = 1m, DefaultValue = 0m,
|
||
IsRequired = false, Sort = sort
|
||
};
|
||
|
||
/// <summary>
|
||
/// 构造一条取消比赛资格原因
|
||
/// </summary>
|
||
private static PrescribedRuleItemDefinition NewDisqualifyItem(string itemCode, string itemName, int sort) => new()
|
||
{
|
||
JudgeRoleType = RoleChief, ItemCode = itemCode, ItemName = itemName,
|
||
ItemType = ScoreItemType.Disqualify, ScoreDimension = ScoreDimensionType.None,
|
||
CalcMode = ScoreCalcMode.RecordOnly,
|
||
IsRequired = false, Sort = sort
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 内置规则里的一条评分项定义
|
||
/// </summary>
|
||
public class PrescribedRuleItemDefinition
|
||
{
|
||
/// <summary>
|
||
/// 裁判角色类别编码
|
||
/// </summary>
|
||
public string JudgeRoleType { get; set; } = null!;
|
||
|
||
/// <summary>
|
||
/// 评分项编码
|
||
/// </summary>
|
||
public string ItemCode { get; set; } = null!;
|
||
|
||
/// <summary>
|
||
/// 评分项名称
|
||
/// </summary>
|
||
public string ItemName { get; set; } = null!;
|
||
|
||
/// <summary>
|
||
/// 评分项类型
|
||
/// </summary>
|
||
public int ItemType { get; set; }
|
||
|
||
/// <summary>
|
||
/// 计入的得分维度
|
||
/// </summary>
|
||
public int ScoreDimension { get; set; }
|
||
|
||
/// <summary>
|
||
/// 计算方式
|
||
/// </summary>
|
||
public int CalcMode { get; set; }
|
||
|
||
/// <summary>
|
||
/// 单位分值
|
||
/// </summary>
|
||
public decimal? UnitValue { get; set; }
|
||
|
||
/// <summary>
|
||
/// 最小值
|
||
/// </summary>
|
||
public decimal? MinValue { get; set; }
|
||
|
||
/// <summary>
|
||
/// 最大值
|
||
/// </summary>
|
||
public decimal? MaxValue { get; set; }
|
||
|
||
/// <summary>
|
||
/// 录入步长
|
||
/// </summary>
|
||
public decimal? StepValue { get; set; }
|
||
|
||
/// <summary>
|
||
/// 默认值
|
||
/// </summary>
|
||
public decimal? DefaultValue { get; set; }
|
||
|
||
/// <summary>
|
||
/// 是否必填
|
||
/// </summary>
|
||
public bool IsRequired { get; set; }
|
||
|
||
/// <summary>
|
||
/// 扩展配置(JSON)
|
||
/// </summary>
|
||
public string? ExtensionJson { get; set; }
|
||
|
||
/// <summary>
|
||
/// 排序
|
||
/// </summary>
|
||
public int Sort { get; set; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// ScoreRuleItem.ItemType 取值
|
||
/// </summary>
|
||
public static class ScoreItemType
|
||
{
|
||
/// <summary>难度等级计数</summary>
|
||
public const int DifficultyCount = 1;
|
||
|
||
/// <summary>规定元素计数</summary>
|
||
public const int RequiredElement = 2;
|
||
|
||
/// <summary>表现维度</summary>
|
||
public const int Performance = 3;
|
||
|
||
/// <summary>扣分项</summary>
|
||
public const int Deduction = 4;
|
||
|
||
/// <summary>取消资格原因</summary>
|
||
public const int Disqualify = 5;
|
||
|
||
/// <summary>规定赛评分项</summary>
|
||
public const int Prescribed = 6;
|
||
}
|
||
|
||
/// <summary>
|
||
/// ScoreRuleItem.ScoreDimension 取值
|
||
/// </summary>
|
||
public static class ScoreDimensionType
|
||
{
|
||
/// <summary>不计入</summary>
|
||
public const int None = 0;
|
||
|
||
/// <summary>难度分</summary>
|
||
public const int Difficulty = 1;
|
||
|
||
/// <summary>表现分</summary>
|
||
public const int Performance = 2;
|
||
|
||
/// <summary>规定分</summary>
|
||
public const int Required = 3;
|
||
|
||
/// <summary>扣分</summary>
|
||
public const int Deduction = 4;
|
||
|
||
/// <summary>
|
||
/// 参与总分计算的四个维度
|
||
/// </summary>
|
||
public static readonly int[] Scoring = { Difficulty, Performance, Required, Deduction };
|
||
}
|
||
|
||
/// <summary>
|
||
/// ScoreRuleItem.CalcMode 取值
|
||
/// </summary>
|
||
public static class ScoreCalcMode
|
||
{
|
||
/// <summary>数量 × 单位分值累加</summary>
|
||
public const int CountByUnit = 0;
|
||
|
||
/// <summary>直接取录入值</summary>
|
||
public const int DirectValue = 1;
|
||
|
||
/// <summary>仅记录不参与合计</summary>
|
||
public const int RecordOnly = 2;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 评分项扩展配置(ScoreRuleItem.ExtensionJson)的读写。
|
||
/// 目前只用于完成质量评分项的个人/集体满分差异。
|
||
/// </summary>
|
||
public static class ScoreItemExtension
|
||
{
|
||
/// <summary>
|
||
/// 生成扩展配置,两个上限都为空时返回 null(不写扩展)
|
||
/// </summary>
|
||
public static string? Build(decimal? individualMax, decimal? teamMax)
|
||
{
|
||
if (individualMax == null && teamMax == null) return null;
|
||
var node = new JObject();
|
||
node["individualMax"] = individualMax == null ? JValue.CreateNull() : new JValue(individualMax.Value);
|
||
node["teamMax"] = teamMax == null ? JValue.CreateNull() : new JValue(teamMax.Value);
|
||
return node.ToString(Formatting.None);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 按项目类型解析评分项满分。
|
||
/// 未配置扩展时返回 fallback(即 ScoreRuleItem.MaxValue);
|
||
/// 配置了扩展但当前项目类型对应的上限为空时返回 null,表示该项目不需要这一项。
|
||
/// </summary>
|
||
public static decimal? ResolveMaxValue(string? extensionJson, bool isTeamProject, decimal? fallback)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(extensionJson)) return fallback;
|
||
JObject node;
|
||
// 扩展配置由后台维护,格式不合法时退回 MaxValue,不影响打分。
|
||
try { node = JObject.Parse(extensionJson); } catch (JsonException) { return fallback; }
|
||
var key = isTeamProject ? "teamMax" : "individualMax";
|
||
var token = node[key];
|
||
if (token == null) return fallback;
|
||
return token.Type == JTokenType.Null ? null : token.Value<decimal>();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 判断评分项是否配置了个人/集体差异化满分
|
||
/// </summary>
|
||
public static bool HasProjectTypeMax(string? extensionJson)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(extensionJson)) return false;
|
||
try { return JObject.Parse(extensionJson)["individualMax"] != null || JObject.Parse(extensionJson)["teamMax"] != null; }
|
||
catch (JsonException) { return false; }
|
||
}
|
||
}
|