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":"T","name":"主裁判","min":1,"max":1}]},{"title":"创意编排裁判","slots":[{"code":"CJ","name":"创意编排裁判","min":1}]},{"title":"完成质量裁判","slots":[{"code":"CQ","name":"完成质量裁判","min":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; }
}
}