YD_Event.Admin/YD_Event.Application/JudgeScore/PrescribedRuleDefinition.cs

349 lines
14 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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; }
}
}