deepseek wip
This commit is contained in:
+288
-14
@@ -95,7 +95,9 @@ namespace AnotherReplayReader.Utils
|
||||
@"```(?:json)?\s*(\{[\s\S]*?\})\s*```",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
public static AIValidationResult ValidateMachineReadableClaims(string response)
|
||||
public static AIValidationResult ValidateMachineReadableClaims(
|
||||
string response,
|
||||
ReplayFactIndex? factIndex = null)
|
||||
{
|
||||
var issues = ImmutableArray.CreateBuilder<AIValidationIssue>();
|
||||
var json = ExtractJsonObject(response);
|
||||
@@ -115,6 +117,11 @@ namespace AnotherReplayReader.Utils
|
||||
}
|
||||
|
||||
ValidateClaimSelfConsistency(claims, issues);
|
||||
ValidateUnpackAmbiguity(claims, issues);
|
||||
if (factIndex is not null)
|
||||
{
|
||||
ValidateTimelineConsistency(claims, factIndex, issues);
|
||||
}
|
||||
return new AIValidationResult(claims, issues.ToImmutable());
|
||||
}
|
||||
|
||||
@@ -180,11 +187,11 @@ namespace AnotherReplayReader.Utils
|
||||
}
|
||||
|
||||
return new AIMachineReadableClaims(
|
||||
ReadUnitClaims(root),
|
||||
ReadSimpleClaims(root, "eventClaims")
|
||||
ReadUnitClaims(root, issues),
|
||||
ReadSimpleClaims(root, "eventClaims", issues)
|
||||
.Select(c => new AIEventClaim(c.Claim, c.EvidenceLevel, c.Evidence))
|
||||
.ToImmutableArray(),
|
||||
ReadSimpleClaims(root, "timelineClaims")
|
||||
ReadSimpleClaims(root, "timelineClaims", issues)
|
||||
.Select(c => new AITimelineClaim(c.Claim, c.EvidenceLevel, c.Evidence))
|
||||
.ToImmutableArray());
|
||||
}
|
||||
@@ -198,7 +205,9 @@ namespace AnotherReplayReader.Utils
|
||||
}
|
||||
}
|
||||
|
||||
private static ImmutableArray<AIUnitClaim> ReadUnitClaims(JsonElement root)
|
||||
private const int MaxUnitClaims = 10;
|
||||
|
||||
private static ImmutableArray<AIUnitClaim> ReadUnitClaims(JsonElement root, ImmutableArray<AIValidationIssue>.Builder issues)
|
||||
{
|
||||
if (!TryGetArray(root, "unitClaims", out var unitClaims))
|
||||
{
|
||||
@@ -206,6 +215,7 @@ namespace AnotherReplayReader.Utils
|
||||
}
|
||||
|
||||
var result = ImmutableArray.CreateBuilder<AIUnitClaim>();
|
||||
var totalCount = 0;
|
||||
foreach (var item in unitClaims.EnumerateArray())
|
||||
{
|
||||
if (item.ValueKind != JsonValueKind.Object)
|
||||
@@ -213,15 +223,30 @@ namespace AnotherReplayReader.Utils
|
||||
continue;
|
||||
}
|
||||
|
||||
totalCount++;
|
||||
if (result.Count >= MaxUnitClaims)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
result.Add(new AIUnitClaim(
|
||||
ReadFlexibleString(item, "unitId"),
|
||||
ReadFlexibleString(item, "player"),
|
||||
ReadFlexibleString(item, "claim"),
|
||||
ReadEvidenceLevel(item),
|
||||
ReadEvidenceLevel(item, issues),
|
||||
ReadStringArray(item, "evidence"),
|
||||
ReadStringArray(item, "alternatives"),
|
||||
ReadStringArray(item, "needsConfirmation")));
|
||||
}
|
||||
|
||||
if (totalCount > MaxUnitClaims)
|
||||
{
|
||||
issues.Add(new AIValidationIssue(
|
||||
AIValidationSeverity.Info,
|
||||
AIValidationIssueKind.InvalidMachineReadableClaims,
|
||||
$"unitClaims 包含 {totalCount} 条声明,仅处理前 {MaxUnitClaims} 条,其余已忽略。"));
|
||||
}
|
||||
|
||||
return result.ToImmutable();
|
||||
}
|
||||
|
||||
@@ -230,7 +255,7 @@ namespace AnotherReplayReader.Utils
|
||||
AIEvidenceLevel EvidenceLevel,
|
||||
ImmutableArray<string> Evidence);
|
||||
|
||||
private static ImmutableArray<SimpleClaim> ReadSimpleClaims(JsonElement root, string propertyName)
|
||||
private static ImmutableArray<SimpleClaim> ReadSimpleClaims(JsonElement root, string propertyName, ImmutableArray<AIValidationIssue>.Builder issues)
|
||||
{
|
||||
if (!TryGetArray(root, propertyName, out var claims))
|
||||
{
|
||||
@@ -238,6 +263,13 @@ namespace AnotherReplayReader.Utils
|
||||
}
|
||||
|
||||
var result = ImmutableArray.CreateBuilder<SimpleClaim>();
|
||||
var maxClaims = propertyName switch
|
||||
{
|
||||
"eventClaims" => 5,
|
||||
"timelineClaims" => 3,
|
||||
_ => 50,
|
||||
};
|
||||
var totalCount = 0;
|
||||
foreach (var item in claims.EnumerateArray())
|
||||
{
|
||||
if (item.ValueKind != JsonValueKind.Object)
|
||||
@@ -245,11 +277,32 @@ namespace AnotherReplayReader.Utils
|
||||
continue;
|
||||
}
|
||||
|
||||
totalCount++;
|
||||
if (result.Count >= maxClaims)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var claim = ReadFlexibleString(item, "claim");
|
||||
if (string.IsNullOrWhiteSpace(claim) && propertyName == "eventClaims")
|
||||
{
|
||||
claim = ReadFlexibleString(item, "event");
|
||||
}
|
||||
|
||||
result.Add(new SimpleClaim(
|
||||
ReadFlexibleString(item, "claim"),
|
||||
ReadEvidenceLevel(item),
|
||||
claim,
|
||||
ReadEvidenceLevel(item, issues),
|
||||
ReadStringArray(item, "evidence")));
|
||||
}
|
||||
|
||||
if (totalCount > maxClaims)
|
||||
{
|
||||
issues.Add(new AIValidationIssue(
|
||||
AIValidationSeverity.Info,
|
||||
AIValidationIssueKind.InvalidMachineReadableClaims,
|
||||
$"{propertyName} 包含 {totalCount} 条声明,仅处理前 {maxClaims} 条,其余已忽略。"));
|
||||
}
|
||||
|
||||
return result.ToImmutable();
|
||||
}
|
||||
|
||||
@@ -296,6 +349,216 @@ namespace AnotherReplayReader.Utils
|
||||
}
|
||||
}
|
||||
|
||||
#region Structured evidence parsing
|
||||
|
||||
internal enum AIEvidenceType
|
||||
{
|
||||
Build,
|
||||
Place,
|
||||
Produce,
|
||||
Sell,
|
||||
Select,
|
||||
Move,
|
||||
Power,
|
||||
Unknown
|
||||
}
|
||||
|
||||
internal sealed record StructuredEvidence(
|
||||
AIEvidenceType Type,
|
||||
string Time,
|
||||
ImmutableArray<string> Parameters,
|
||||
string Raw)
|
||||
{
|
||||
public string? GetSpecialPowerName() =>
|
||||
Type == AIEvidenceType.Power && Parameters.Length >= 1 ? Parameters[0] : null;
|
||||
|
||||
public string? GetUnitId() => Type switch
|
||||
{
|
||||
AIEvidenceType.Build or AIEvidenceType.Place when Parameters.Length >= 2 => Parameters[1],
|
||||
AIEvidenceType.Produce when Parameters.Length >= 2 => Parameters[1],
|
||||
AIEvidenceType.Power when Parameters.Length >= 2 => Parameters[1],
|
||||
AIEvidenceType.Sell when Parameters.Length >= 1 => Parameters[0],
|
||||
AIEvidenceType.Select when Parameters.Length >= 1 => Parameters[0],
|
||||
_ => null,
|
||||
};
|
||||
|
||||
public string? GetAssetName() => Type switch
|
||||
{
|
||||
AIEvidenceType.Build or AIEvidenceType.Place or AIEvidenceType.Produce
|
||||
when Parameters.Length >= 1 => Parameters[0],
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
private static readonly Regex _structuredEvidenceRegex = new(
|
||||
@"^(build|place|produce|sell|select|move|power)\|([^|]+(?:\|(?!\|).*)?)$",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
internal static StructuredEvidence ParseStructuredEvidence(string text)
|
||||
{
|
||||
var match = _structuredEvidenceRegex.Match(text.Trim());
|
||||
if (!match.Success)
|
||||
{
|
||||
return new StructuredEvidence(AIEvidenceType.Unknown, string.Empty, ImmutableArray<string>.Empty, text);
|
||||
}
|
||||
|
||||
var type = match.Groups[1].Value.ToLowerInvariant() switch
|
||||
{
|
||||
"build" => AIEvidenceType.Build,
|
||||
"place" => AIEvidenceType.Place,
|
||||
"produce" => AIEvidenceType.Produce,
|
||||
"sell" => AIEvidenceType.Sell,
|
||||
"select" => AIEvidenceType.Select,
|
||||
"move" => AIEvidenceType.Move,
|
||||
"power" => AIEvidenceType.Power,
|
||||
_ => AIEvidenceType.Unknown,
|
||||
};
|
||||
|
||||
var rest = match.Groups[2].Value;
|
||||
var parts = rest.Split('|');
|
||||
var time = parts.Length >= 1 ? parts[0].Trim() : string.Empty;
|
||||
var parameters = parts.Skip(1).Select(p => p.Trim()).ToImmutableArray();
|
||||
|
||||
return new StructuredEvidence(type, time, parameters, text);
|
||||
}
|
||||
|
||||
internal static ImmutableArray<StructuredEvidence> ParseAllEvidence(ImmutableArray<string> evidenceStrings)
|
||||
{
|
||||
return evidenceStrings
|
||||
.Select(ParseStructuredEvidence)
|
||||
.ToImmutableArray();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Validation rules
|
||||
|
||||
private static void ValidateUnpackAmbiguity(
|
||||
AIMachineReadableClaims claims,
|
||||
ImmutableArray<AIValidationIssue>.Builder issues)
|
||||
{
|
||||
foreach (var claim in claims.UnitClaims)
|
||||
{
|
||||
if (claim.EvidenceLevel is not (AIEvidenceLevel.Confirmed or AIEvidenceLevel.HighlyLikely))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var evidence = ParseAllEvidence(claim.Evidence);
|
||||
var hasUnpack = evidence.Any(e =>
|
||||
e.GetSpecialPowerName() is string p &&
|
||||
p.IndexOf("UnpackReplaceSelf", StringComparison.OrdinalIgnoreCase) >= 0);
|
||||
if (!hasUnpack)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var hasPack = evidence.Any(e =>
|
||||
e.GetSpecialPowerName() is string p &&
|
||||
p.IndexOf("PackReplaceSelf", StringComparison.OrdinalIgnoreCase) >= 0);
|
||||
if (hasPack)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
issues.Add(new AIValidationIssue(
|
||||
AIValidationSeverity.WeakEvidence,
|
||||
AIValidationIssueKind.MissingAlternative,
|
||||
$"UnitId {claim.UnitId} 使用了 UnpackReplaceSelf 但证据中无对应 PackReplaceSelf。UnpackReplaceSelf 可能对应基地车展开或矿车展开成指挥中心,建议降低置信度或添加 alternative。",
|
||||
claim.UnitId));
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateTimelineConsistency(
|
||||
AIMachineReadableClaims claims,
|
||||
ReplayFactIndex factIndex,
|
||||
ImmutableArray<AIValidationIssue>.Builder issues)
|
||||
{
|
||||
foreach (var claim in claims.UnitClaims)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(claim.UnitId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!uint.TryParse(claim.UnitId, out var unitId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check 1: UnitId referenced in claim exists in the replay
|
||||
if (!factIndex.UnitIdFirstObservedTime.ContainsKey(unitId))
|
||||
{
|
||||
issues.Add(new AIValidationIssue(
|
||||
AIValidationSeverity.Warning,
|
||||
AIValidationIssueKind.InvalidMachineReadableClaims,
|
||||
$"UnitId {claim.UnitId} 在回放数据中从未出现过,AI 可能编造了不存在的 UnitId。",
|
||||
claim.UnitId));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check 2: Verify special power evidence against fact index
|
||||
var evidence = ParseAllEvidence(claim.Evidence);
|
||||
foreach (var ev in evidence)
|
||||
{
|
||||
if (ev.Type != AIEvidenceType.Power)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var evUnitIdStr = ev.GetUnitId();
|
||||
if (evUnitIdStr is null || !uint.TryParse(evUnitIdStr, out var evUnitId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var powerName = ev.GetSpecialPowerName();
|
||||
if (powerName is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Does this UnitId exist in the fact index?
|
||||
if (!factIndex.UnitIdFirstObservedTime.ContainsKey(evUnitId))
|
||||
{
|
||||
issues.Add(new AIValidationIssue(
|
||||
AIValidationSeverity.Info,
|
||||
AIValidationIssueKind.InvalidMachineReadableClaims,
|
||||
$"证据引用了回放中不存在的 UnitId {evUnitIdStr}。",
|
||||
claim.UnitId));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Did this UnitId actually use this special power?
|
||||
if (factIndex.UnitIdSpecialPowers.TryGetValue(evUnitId, out var actualPowers)
|
||||
&& !actualPowers.Contains(powerName))
|
||||
{
|
||||
issues.Add(new AIValidationIssue(
|
||||
AIValidationSeverity.Contradiction,
|
||||
AIValidationIssueKind.UnitCapabilityContradiction,
|
||||
$"UnitId {evUnitIdStr} 在回放中使用过以下特殊能力:{string.Join(", ", actualPowers.OrderBy(x => x))},但 AI 声称其使用了“{powerName}”——此能力未在该 UnitId 上观察到。",
|
||||
claim.UnitId));
|
||||
}
|
||||
}
|
||||
|
||||
// Check 3: UnitId used as builder vs claim
|
||||
var isBuilderInReplay = factIndex.BuilderUnitIds.Contains(unitId);
|
||||
var claimLooksLikeBuilder = claim.Claim.IndexOf("MCV", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| claim.Claim.IndexOf("基地车", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| claim.Claim.IndexOf("Nanocore", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| claim.Claim.IndexOf("纳米核心", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| claim.Claim.IndexOf("builder", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| claim.Claim.IndexOf("建造者", StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
if (claimLooksLikeBuilder && !isBuilderInReplay)
|
||||
{
|
||||
issues.Add(new AIValidationIssue(
|
||||
AIValidationSeverity.WeakEvidence,
|
||||
AIValidationIssueKind.UnitCapabilityContradiction,
|
||||
$"AI 推测 UnitId {claim.UnitId} 是“{claim.Claim}”(推测是建造单位),但该 UnitId 在回放中从未作为建造者(建造建筑)出现。",
|
||||
claim.UnitId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private static bool TryGetArray(JsonElement root, string propertyName, out JsonElement array)
|
||||
{
|
||||
if (root.TryGetProperty(propertyName, out array)
|
||||
@@ -308,23 +571,34 @@ namespace AnotherReplayReader.Utils
|
||||
return false;
|
||||
}
|
||||
|
||||
private static AIEvidenceLevel ReadEvidenceLevel(JsonElement item)
|
||||
private static AIEvidenceLevel ReadEvidenceLevel(JsonElement item, ImmutableArray<AIValidationIssue>.Builder issues)
|
||||
{
|
||||
var value = ReadFlexibleString(item, "evidenceLevel");
|
||||
return NormalizeEvidenceLevel(value);
|
||||
return NormalizeEvidenceLevel(value, issues);
|
||||
}
|
||||
|
||||
private static AIEvidenceLevel NormalizeEvidenceLevel(string value)
|
||||
private static AIEvidenceLevel NormalizeEvidenceLevel(string value, ImmutableArray<AIValidationIssue>.Builder issues)
|
||||
{
|
||||
value = value.Trim().Replace("_", "").Replace("-", "").Replace(" ", "");
|
||||
return value.ToLowerInvariant() switch
|
||||
var result = value.ToLowerInvariant() switch
|
||||
{
|
||||
"confirmed" or "确定" => AIEvidenceLevel.Confirmed,
|
||||
"highlylikely" or "high" or "高度可能" => AIEvidenceLevel.HighlyLikely,
|
||||
"possible" or "可能" => AIEvidenceLevel.Possible,
|
||||
"ruledout" or "excluded" or "已排除" => AIEvidenceLevel.RuledOut,
|
||||
_ => AIEvidenceLevel.Uncertain,
|
||||
_ => (AIEvidenceLevel?)null,
|
||||
};
|
||||
|
||||
if (result is not null)
|
||||
{
|
||||
return result.Value;
|
||||
}
|
||||
|
||||
issues.Add(new AIValidationIssue(
|
||||
AIValidationSeverity.Info,
|
||||
AIValidationIssueKind.InvalidEvidenceLevel,
|
||||
$"无法识别的证据等级 \"{value.Trim()}\",已降级为不确定。"));
|
||||
return AIEvidenceLevel.Uncertain;
|
||||
}
|
||||
|
||||
private static string ReadFlexibleString(JsonElement item, string propertyName)
|
||||
|
||||
+32
-6
@@ -57,7 +57,7 @@ namespace AnotherReplayReader.Utils
|
||||
- 你只能根据用户提供的操作记录、玩家信息、下方游戏规则和明确给出的背景知识进行分析。
|
||||
- 不要使用现实世界常识或其他 RTS 游戏常识覆盖这里的游戏设定。例如:步兵、直升机、建筑水陆摆放、运输能力、两栖能力都必须以这里的规则和单位描述为准。
|
||||
- 不确定时必须保留多个候选,不要为了让解说流畅而过早下定论。
|
||||
- 对 UnitId、单位类型、战术意图的判断必须区分证据等级:确定、高度可能、可能、待确认、已排除。
|
||||
- 对 UnitId、单位类型、战术意图的判断必须区分证据等级:确定、高度可能、可能、不确定、已排除。
|
||||
- 每个关键推理都应当包含支持证据;如果存在会推翻该推理的反证,也要主动指出。
|
||||
- 如果某个技能或行为可以对应多个单位,先列出候选,并说明还需要哪些后续迹象才能确认。
|
||||
- 对已经被操作记录直接否定的判断必须修正或放弃,不要坚持原结论。
|
||||
@@ -130,6 +130,7 @@ namespace AnotherReplayReader.Utils
|
||||
# 机器可读声明
|
||||
仅限于:分段分析阶段(第2阶段)
|
||||
如果你对 UnitId、关键事件或时间线做出了可验证推测,请在回答末尾附加下面格式。
|
||||
请限制推测数量:UnitId 推测不超过 10 个,事件推测不超过 5 个,时间线推测不超过 3 个。
|
||||
必须先输出一行`[机器可读声明]`,然后输出一个 JSON 代码块:
|
||||
[机器可读声明]
|
||||
```json
|
||||
@@ -140,15 +141,40 @@ namespace AnotherReplayReader.Utils
|
||||
""player"": ""PlayerA"",
|
||||
""claim"": ""AlliedMCV"",
|
||||
""evidenceLevel"": ""possible"",
|
||||
""evidence"": [""8:30 使用 SpecialPower_UnpackReplaceSelf""],
|
||||
""evidence"": [""power|1:24.00|SpecialPower_PackReplaceSelf|246"", ""power|1:41.00|SpecialPower_UnpackReplaceSelf|123""],
|
||||
""alternatives"": [""AlliedMiner 展开后的指挥中心""],
|
||||
""needsConfirmation"": [""是否曾使用 SpecialPower_PackReplaceSelf"", ""后续是否作为建造者出现""]
|
||||
""needsConfirmation"": [""是否曾作为建造者出现""]
|
||||
}
|
||||
],
|
||||
""eventClaims"": [],
|
||||
""timelineClaims"": []
|
||||
""eventClaims"": [
|
||||
{
|
||||
""claim"": ""PlayerA 主基地打包并开始迁移"",
|
||||
""evidenceLevel"": ""confirmed"",
|
||||
""evidence"": [""power|1:24.00|SpecialPower_PackReplaceSelf|246""]
|
||||
}
|
||||
],
|
||||
""timelineClaims"": [
|
||||
{
|
||||
""claim"": ""PlayerA 在 2 分钟内完成基地迁移"",
|
||||
""evidenceLevel"": ""confirmed"",
|
||||
""evidence"": [""power|1:24.00|SpecialPower_PackReplaceSelf|246"", ""power|1:41.00|SpecialPower_UnpackReplaceSelf|123""]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
- `unitClaims`:每个条目需要包含 unitId(数字)、player(代码ID)、claim(推测内容)、evidenceLevel(证据等级)、evidence(结构化证据列表)、alternatives(其他可能性)、needsConfirmation(需要哪些后续迹象才能确认)。
|
||||
- `eventClaims`:每个条目需要包含 claim(事件描述)、evidenceLevel(证据等级)、evidence(结构化证据列表)。
|
||||
- `timelineClaims`:每个条目需要包含 claim(时间线描述)、evidenceLevel(证据等级)、evidence(结构化证据列表)。
|
||||
|
||||
**evidence 格式**:每条 evidence 必须是以下 pipe 分隔格式之一,不允许使用自然语言描述:
|
||||
- `build|时间|建筑名|建造者UnitId` — 开始建造建筑,例如 `build|0:01.26|AlliedBarracks|246`
|
||||
- `place|时间|建筑名|建造者UnitId|x,y,z` — 摆放建筑,例如 `place|0:01.46|AlliedBarracks|246|1905,2231,210`
|
||||
- `produce|时间|单位名|出兵建筑UnitId` — 开始出兵,例如 `produce|0:14.66|AlliedScoutInfantry|291`
|
||||
- `sell|时间|建筑UnitId` — 出售建筑,例如 `sell|2:21.93|255`
|
||||
- `select|时间|单位UnitId` — 选择单位,例如 `select|1:24.13|587`
|
||||
- `move|时间|x,y,z` — 移动,例如 `move|1:24.26|2026,2800,280`
|
||||
- `power|时间|技能名|单位UnitId` — 释放特殊能力,例如 `power|1:24.00|SpecialPower_PackReplaceSelf|246`
|
||||
|
||||
如果没有可验证推测,可以输出空数组。不要在 JSON 里写注释。
|
||||
|
||||
# 推理指南
|
||||
@@ -1015,7 +1041,7 @@ PlayerA: 开始出兵
|
||||
你需要列出{beginText}至{endText}的主要事件、以及其他有分析价值的事件。
|
||||
请按照按照[观察]、[分析]、[推理]、[进一步思考(可选)]的步骤,对各个事件进行分析和推理。
|
||||
|
||||
假如当前阶段存在一些较为重要的单位、而且能够推测出它们可能是什么单位,则可以列出单位的UnitId以及你对单位的推测
|
||||
假如当前阶段存在一些较为重要的单位、而且能够推测出它们可能是什么单位,则可以列出单位的UnitId以及你对单位的推测。UnitId 推测最多 10 个,事件推测最多 5 个,时间线推测最多 3 个。
|
||||
如果你列出了 UnitId 推测、关键事件推测或时间线推测,请在回答末尾附加`[机器可读声明]` JSON 代码块;如果没有相关推测,则输出空数组。
|
||||
";
|
||||
return instruction.Trim().Replace("\r", "");
|
||||
|
||||
@@ -0,0 +1,385 @@
|
||||
using AnotherReplayReader.ReplayFile;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
|
||||
namespace AnotherReplayReader.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// Index of replay facts extracted from CommandChunk data.
|
||||
/// Used by AIAnalysisValidation to cross-reference LLM claims against
|
||||
/// actual replay operations.
|
||||
/// </summary>
|
||||
internal sealed class ReplayFactIndex
|
||||
{
|
||||
/// <summary>First time a UnitId was observed in any command.</summary>
|
||||
public ImmutableDictionary<uint, TimeSpan> UnitIdFirstObservedTime { get; }
|
||||
|
||||
/// <summary>Special powers used by each UnitId.</summary>
|
||||
public ImmutableDictionary<uint, ImmutableHashSet<string>> UnitIdSpecialPowers { get; }
|
||||
|
||||
/// <summary>UnitIds that appeared as builder ("建造者") in construction commands.</summary>
|
||||
public ImmutableHashSet<uint> BuilderUnitIds { get; }
|
||||
|
||||
/// <summary>UnitIds that appeared as production structures ("出兵建筑").</summary>
|
||||
public ImmutableHashSet<uint> ProducerUnitIds { get; }
|
||||
|
||||
/// <summary>Per player, per unit asset name, first production start time.</summary>
|
||||
public ImmutableDictionary<int, ImmutableDictionary<string, TimeSpan>> PlayerFirstProductionTime { get; }
|
||||
|
||||
/// <summary>Per player, which UnitIds they have selected.</summary>
|
||||
public ImmutableDictionary<int, ImmutableHashSet<uint>> PlayerSelectedUnitIds { get; }
|
||||
|
||||
public ReplayFactIndex(
|
||||
ImmutableDictionary<uint, TimeSpan> unitIdFirstObservedTime,
|
||||
ImmutableDictionary<uint, ImmutableHashSet<string>> unitIdSpecialPowers,
|
||||
ImmutableHashSet<uint> builderUnitIds,
|
||||
ImmutableHashSet<uint> producerUnitIds,
|
||||
ImmutableDictionary<int, ImmutableDictionary<string, TimeSpan>> playerFirstProductionTime,
|
||||
ImmutableDictionary<int, ImmutableHashSet<uint>> playerSelectedUnitIds)
|
||||
{
|
||||
UnitIdFirstObservedTime = unitIdFirstObservedTime;
|
||||
UnitIdSpecialPowers = unitIdSpecialPowers;
|
||||
BuilderUnitIds = builderUnitIds;
|
||||
ProducerUnitIds = producerUnitIds;
|
||||
PlayerFirstProductionTime = playerFirstProductionTime;
|
||||
PlayerSelectedUnitIds = playerSelectedUnitIds;
|
||||
}
|
||||
|
||||
public static ReplayFactIndex Build(
|
||||
ImmutableArray<(TimeSpan Time, ImmutableArray<CommandChunk> Commands)> timeline,
|
||||
IReadOnlyDictionary<uint, string> stringHashTable)
|
||||
{
|
||||
var unitFirstObserved = new Dictionary<uint, TimeSpan>();
|
||||
var unitSpecialPowers = new Dictionary<uint, HashSet<string>>();
|
||||
var builderUnits = new HashSet<uint>();
|
||||
var producerUnits = new HashSet<uint>();
|
||||
var playerFirstProduction = new Dictionary<int, Dictionary<string, TimeSpan>>();
|
||||
var playerSelected = new Dictionary<int, HashSet<uint>>();
|
||||
|
||||
foreach (var (time, commands) in timeline)
|
||||
{
|
||||
foreach (var command in commands)
|
||||
{
|
||||
ProcessCommand(time, command, stringHashTable,
|
||||
unitFirstObserved, unitSpecialPowers,
|
||||
builderUnits, producerUnits,
|
||||
playerFirstProduction, playerSelected);
|
||||
}
|
||||
}
|
||||
|
||||
return new ReplayFactIndex(
|
||||
unitFirstObserved.ToImmutableDictionary(),
|
||||
unitSpecialPowers.ToImmutableDictionary(
|
||||
kv => kv.Key, kv => kv.Value.ToImmutableHashSet()),
|
||||
builderUnits.ToImmutableHashSet(),
|
||||
producerUnits.ToImmutableHashSet(),
|
||||
playerFirstProduction.ToImmutableDictionary(
|
||||
kv => kv.Key, kv => kv.Value.ToImmutableDictionary()),
|
||||
playerSelected.ToImmutableDictionary(
|
||||
kv => kv.Key, kv => kv.Value.ToImmutableHashSet()));
|
||||
}
|
||||
|
||||
private static void ProcessCommand(
|
||||
TimeSpan time,
|
||||
CommandChunk command,
|
||||
IReadOnlyDictionary<uint, string> stringHashTable,
|
||||
Dictionary<uint, TimeSpan> unitFirstObserved,
|
||||
Dictionary<uint, HashSet<string>> unitSpecialPowers,
|
||||
HashSet<uint> builderUnits,
|
||||
HashSet<uint> producerUnits,
|
||||
Dictionary<int, Dictionary<string, TimeSpan>> playerFirstProduction,
|
||||
Dictionary<int, HashSet<uint>> playerSelected)
|
||||
{
|
||||
var player = command.PlayerIndex;
|
||||
var cmdId = command.CommandId;
|
||||
|
||||
switch (cmdId)
|
||||
{
|
||||
// select unit(s): 0x1F5
|
||||
case 0x1F5:
|
||||
RecordSelectUnit(time, command, player, unitFirstObserved, playerSelected);
|
||||
break;
|
||||
|
||||
// special power (no target): 0x1FE
|
||||
case 0x1FE:
|
||||
RecordSpecialPower(time, command, stringHashTable, unitFirstObserved, unitSpecialPowers);
|
||||
break;
|
||||
|
||||
// special power (target position): 0x1FF
|
||||
case 0x1FF:
|
||||
RecordSpecialPower(time, command, stringHashTable, unitFirstObserved, unitSpecialPowers);
|
||||
break;
|
||||
|
||||
// special power (target position and angle): 0x200
|
||||
case 0x200:
|
||||
RecordSpecialPower(time, command, stringHashTable, unitFirstObserved, unitSpecialPowers);
|
||||
break;
|
||||
|
||||
// special power (target unit): 0x201
|
||||
case 0x201:
|
||||
RecordSpecialPower(time, command, stringHashTable, unitFirstObserved, unitSpecialPowers);
|
||||
break;
|
||||
|
||||
// special power (one or more targets): 0x232
|
||||
case 0x232:
|
||||
RecordSpecialPower(time, command, stringHashTable, unitFirstObserved, unitSpecialPowers);
|
||||
break;
|
||||
|
||||
// start production: 0x205
|
||||
case 0x205:
|
||||
RecordProduction(time, command, player, unitFirstObserved, producerUnits, playerFirstProduction);
|
||||
break;
|
||||
|
||||
// start construction: 0x207
|
||||
case 0x207:
|
||||
RecordConstruction(time, command, unitFirstObserved, builderUnits);
|
||||
break;
|
||||
|
||||
// place building: 0x209
|
||||
case 0x209:
|
||||
RecordPlaceBuilding(time, command, unitFirstObserved, builderUnits);
|
||||
break;
|
||||
|
||||
// sell building: 0x20A
|
||||
case 0x20A:
|
||||
RecordObjectReference(time, command, unitFirstObserved);
|
||||
break;
|
||||
|
||||
// move: 0x214
|
||||
case 0x214:
|
||||
// attack move: 0x215
|
||||
case 0x215:
|
||||
// These commands operate on currently selected units.
|
||||
// The target is a position, not a UnitId.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void RecordSelectUnit(
|
||||
TimeSpan time,
|
||||
CommandChunk command,
|
||||
int player,
|
||||
Dictionary<uint, TimeSpan> unitFirstObserved,
|
||||
Dictionary<int, HashSet<uint>> playerSelected)
|
||||
{
|
||||
// Data layout for 0x1F5:
|
||||
// Data[0]: Bool (isReplace), if count > 0 the rest are ObjectIds
|
||||
// Data[1..]: ObjectIds of selected units
|
||||
foreach (var entry in command.Data)
|
||||
{
|
||||
if (entry.Type is CommandArgumentType.ObjectId or CommandArgumentType.ObjectId_2)
|
||||
{
|
||||
if (entry.Count == 1)
|
||||
{
|
||||
var unitId = (uint)entry.Value;
|
||||
TryRecordFirstObserved(unitId, time, unitFirstObserved);
|
||||
RecordPlayerSelection(player, unitId, playerSelected);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var id in (uint[])entry.Value)
|
||||
{
|
||||
TryRecordFirstObserved(id, time, unitFirstObserved);
|
||||
RecordPlayerSelection(player, id, playerSelected);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void RecordSpecialPower(
|
||||
TimeSpan time,
|
||||
CommandChunk command,
|
||||
IReadOnlyDictionary<uint, string> stringHashTable,
|
||||
Dictionary<uint, TimeSpan> unitFirstObserved,
|
||||
Dictionary<uint, HashSet<string>> unitSpecialPowers)
|
||||
{
|
||||
string? powerName = null;
|
||||
var unitIds = new List<uint>();
|
||||
|
||||
foreach (var entry in command.Data)
|
||||
{
|
||||
switch (entry.Type)
|
||||
{
|
||||
case CommandArgumentType.Int32 when powerName is null:
|
||||
{
|
||||
// First Int32 is the special power hash ID
|
||||
var hash = unchecked((uint)(int)entry.Value);
|
||||
powerName = stringHashTable.TryGetValue(hash, out var name)
|
||||
? name
|
||||
: $"Hash_{hash:X8}";
|
||||
break;
|
||||
}
|
||||
case CommandArgumentType.ObjectId or CommandArgumentType.ObjectId_2:
|
||||
{
|
||||
if (entry.Count == 1)
|
||||
{
|
||||
var id = (uint)entry.Value;
|
||||
if (id != 0) unitIds.Add(id);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var id in (uint[])entry.Value)
|
||||
{
|
||||
if (id != 0) unitIds.Add(id);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (powerName is null || unitIds.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var unitId in unitIds)
|
||||
{
|
||||
TryRecordFirstObserved(unitId, time, unitFirstObserved);
|
||||
if (!unitSpecialPowers.TryGetValue(unitId, out var powers))
|
||||
{
|
||||
powers = new HashSet<string>();
|
||||
unitSpecialPowers[unitId] = powers;
|
||||
}
|
||||
powers.Add(powerName);
|
||||
}
|
||||
}
|
||||
|
||||
private static void RecordProduction(
|
||||
TimeSpan time,
|
||||
CommandChunk command,
|
||||
int player,
|
||||
Dictionary<uint, TimeSpan> unitFirstObserved,
|
||||
HashSet<uint> producerUnits,
|
||||
Dictionary<int, Dictionary<string, TimeSpan>> playerFirstProduction)
|
||||
{
|
||||
uint? producerId = null;
|
||||
string? unitName = null;
|
||||
|
||||
foreach (var entry in command.Data)
|
||||
{
|
||||
switch (entry.Type)
|
||||
{
|
||||
case CommandArgumentType.ObjectId or CommandArgumentType.ObjectId_2
|
||||
when entry.Count == 1 && producerId is null:
|
||||
producerId = (uint)entry.Value;
|
||||
break;
|
||||
case CommandArgumentType.AsciiString or CommandArgumentType.UnicodeString
|
||||
when unitName is null:
|
||||
unitName = entry.Value.ToString() ?? string.Empty;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (producerId.HasValue)
|
||||
{
|
||||
TryRecordFirstObserved(producerId.Value, time, unitFirstObserved);
|
||||
producerUnits.Add(producerId.Value);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(unitName))
|
||||
{
|
||||
if (!playerFirstProduction.TryGetValue(player, out var perPlayer))
|
||||
{
|
||||
perPlayer = new Dictionary<string, TimeSpan>();
|
||||
playerFirstProduction[player] = perPlayer;
|
||||
}
|
||||
if (!perPlayer.ContainsKey(unitName))
|
||||
{
|
||||
perPlayer[unitName] = time;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void RecordConstruction(
|
||||
TimeSpan time,
|
||||
CommandChunk command,
|
||||
Dictionary<uint, TimeSpan> unitFirstObserved,
|
||||
HashSet<uint> builderUnits)
|
||||
{
|
||||
// Data[0]: ObjectId (builder)
|
||||
// Data[1]: AsciiString (building name)
|
||||
RecordBuilder(time, command, unitFirstObserved, builderUnits);
|
||||
}
|
||||
|
||||
private static void RecordPlaceBuilding(
|
||||
TimeSpan time,
|
||||
CommandChunk command,
|
||||
Dictionary<uint, TimeSpan> unitFirstObserved,
|
||||
HashSet<uint> builderUnits)
|
||||
{
|
||||
// Data[0]: ObjectId (builder)
|
||||
// Data[1]: AsciiString (building name)
|
||||
// Data[2]: Int32 (count)
|
||||
// Data[3]: Vector3 (position)
|
||||
// Data[4]: Float32 (angle)
|
||||
RecordBuilder(time, command, unitFirstObserved, builderUnits);
|
||||
}
|
||||
|
||||
private static void RecordBuilder(
|
||||
TimeSpan time,
|
||||
CommandChunk command,
|
||||
Dictionary<uint, TimeSpan> unitFirstObserved,
|
||||
HashSet<uint> builderUnits)
|
||||
{
|
||||
foreach (var entry in command.Data)
|
||||
{
|
||||
if (entry.Type is CommandArgumentType.ObjectId or CommandArgumentType.ObjectId_2
|
||||
&& entry.Count == 1)
|
||||
{
|
||||
var id = (uint)entry.Value;
|
||||
if (id != 0)
|
||||
{
|
||||
TryRecordFirstObserved(id, time, unitFirstObserved);
|
||||
builderUnits.Add(id);
|
||||
}
|
||||
return; // only first ObjectId is the builder
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void RecordObjectReference(
|
||||
TimeSpan time,
|
||||
CommandChunk command,
|
||||
Dictionary<uint, TimeSpan> unitFirstObserved)
|
||||
{
|
||||
foreach (var entry in command.Data)
|
||||
{
|
||||
if (entry.Type is CommandArgumentType.ObjectId or CommandArgumentType.ObjectId_2)
|
||||
{
|
||||
if (entry.Count == 1)
|
||||
{
|
||||
TryRecordFirstObserved((uint)entry.Value, time, unitFirstObserved);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var id in (uint[])entry.Value)
|
||||
{
|
||||
TryRecordFirstObserved(id, time, unitFirstObserved);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void RecordPlayerSelection(int player, uint unitId, Dictionary<int, HashSet<uint>> playerSelected)
|
||||
{
|
||||
if (!playerSelected.TryGetValue(player, out var set))
|
||||
{
|
||||
set = new HashSet<uint>();
|
||||
playerSelected[player] = set;
|
||||
}
|
||||
set.Add(unitId);
|
||||
}
|
||||
|
||||
private static void TryRecordFirstObserved(uint unitId, TimeSpan time, Dictionary<uint, TimeSpan> unitFirstObserved)
|
||||
{
|
||||
if (unitId == 0) return;
|
||||
if (unitFirstObserved.ContainsKey(unitId)) return;
|
||||
unitFirstObserved[unitId] = time;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user