namespace YD_Event.Application.JudgeScore; /// /// 规定赛(花样跳绳规定套路)内置计分规则定义。 /// /// 只在初始化/同步规则库时使用;运行期的评分项一律从 ScoreRule / ScoreRuleItem 读取, /// 后台调整评分项、分值上限、扣分单位后无需改代码。 /// /// /// 计分口径(原型 + 会议纪要):
/// T1 = 创意编排裁判(CJ)各自“创意编排分”的平均值,四舍五入保留两位小数;
/// T2 = 完成质量裁判(CQ)各自“完成质量分”的平均值,四舍五入保留两位小数;
/// T3 = 主裁(T)判定的违例项次 × 每项 5 分;
/// 规定赛总分 = T1 + T2 - T3。 ///
///
public static class PrescribedRuleDefinition { /// /// 规定赛规则编码 /// public const string RuleCode = "ROPE_PRESCRIBED"; /// /// 规定赛规则名称 /// public const string RuleName = "规定赛计分规则"; /// /// 创意编排裁判角色类别编码 /// public const string RoleChoreography = "CJ"; /// /// 完成质量裁判角色类别编码 /// public const string RoleCompletionQuality = "CQ"; /// /// 主裁角色类别编码 /// public const string RoleChief = "T"; /// /// 每项违例的扣分值(主裁每判定一项违例扣 5 分) /// public const decimal ViolationUnitDeduction = 5m; /// /// 裁判席位配置。 /// CJ / CQ 不限人数(成绩按平均值计算,标准配置各 3 人写在 RuleConfigJson 里),主裁固定 1 人。 /// 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}]}] """; /// /// 计分参数配置。 /// roles 描述每个裁判类别在打分页与聚合计算中的表现: /// dimension 对应 ScoreRuleItem.ScoreDimension;aggregateMode=score 表示页面合计展示分值,count 表示展示项次数量; /// standardCount 是标准配置人数(仅用于展示“已提交 x/3”),实际参与平均的人数以已提交评分单为准。 /// 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分"}]} """; /// /// 规定赛全部评分项。名称与裁判端原型逐字一致,改动请同步原型。 /// public static IReadOnlyList Items { get; } = BuildItems(); /// /// 构造规定赛评分项清单 /// private static List BuildItems() { var items = new List(); 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; } /// /// 构造一条完成质量评分项,个人/集体满分写进扩展配置,运行期按项目类型取值 /// 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) }; /// /// 构造一条主裁违例扣分项,录入数量后按每项 5 分累加 /// 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 }; /// /// 构造一条取消比赛资格原因 /// 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 }; } /// /// 内置规则里的一条评分项定义 /// public class PrescribedRuleItemDefinition { /// /// 裁判角色类别编码 /// public string JudgeRoleType { get; set; } = null!; /// /// 评分项编码 /// public string ItemCode { get; set; } = null!; /// /// 评分项名称 /// public string ItemName { get; set; } = null!; /// /// 评分项类型 /// public int ItemType { get; set; } /// /// 计入的得分维度 /// public int ScoreDimension { get; set; } /// /// 计算方式 /// public int CalcMode { get; set; } /// /// 单位分值 /// public decimal? UnitValue { get; set; } /// /// 最小值 /// public decimal? MinValue { get; set; } /// /// 最大值 /// public decimal? MaxValue { get; set; } /// /// 录入步长 /// public decimal? StepValue { get; set; } /// /// 默认值 /// public decimal? DefaultValue { get; set; } /// /// 是否必填 /// public bool IsRequired { get; set; } /// /// 扩展配置(JSON) /// public string? ExtensionJson { get; set; } /// /// 排序 /// public int Sort { get; set; } } /// /// ScoreRuleItem.ItemType 取值 /// public static class ScoreItemType { /// 难度等级计数 public const int DifficultyCount = 1; /// 规定元素计数 public const int RequiredElement = 2; /// 表现维度 public const int Performance = 3; /// 扣分项 public const int Deduction = 4; /// 取消资格原因 public const int Disqualify = 5; /// 规定赛评分项 public const int Prescribed = 6; } /// /// ScoreRuleItem.ScoreDimension 取值 /// public static class ScoreDimensionType { /// 不计入 public const int None = 0; /// 难度分 public const int Difficulty = 1; /// 表现分 public const int Performance = 2; /// 规定分 public const int Required = 3; /// 扣分 public const int Deduction = 4; /// /// 参与总分计算的四个维度 /// public static readonly int[] Scoring = { Difficulty, Performance, Required, Deduction }; } /// /// ScoreRuleItem.CalcMode 取值 /// public static class ScoreCalcMode { /// 数量 × 单位分值累加 public const int CountByUnit = 0; /// 直接取录入值 public const int DirectValue = 1; /// 仅记录不参与合计 public const int RecordOnly = 2; } /// /// 评分项扩展配置(ScoreRuleItem.ExtensionJson)的读写。 /// 目前只用于完成质量评分项的个人/集体满分差异。 /// public static class ScoreItemExtension { /// /// 生成扩展配置,两个上限都为空时返回 null(不写扩展) /// 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); } /// /// 按项目类型解析评分项满分。 /// 未配置扩展时返回 fallback(即 ScoreRuleItem.MaxValue); /// 配置了扩展但当前项目类型对应的上限为空时返回 null,表示该项目不需要这一项。 /// 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(); } /// /// 判断评分项是否配置了个人/集体差异化满分 /// 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; } } }