Skip to content

Commit 2e27cd0

Browse files
committed
build from fen-like string
1 parent cbbaae7 commit 2e27cd0

File tree

16 files changed

+724
-147
lines changed

16 files changed

+724
-147
lines changed

src/dsstats.db8cli/Program.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,18 @@ static void Main(string[] args)
8787
from rp in r.ReplayPlayers
8888
from s in rp.Spawns
8989
from su in s.Units
90-
where r.GameTime > minDate && rp.Team == 1 && rp.Race == Commander.Zerg && s.Breakpoint == Breakpoint.All
90+
where r.GameTime > minDate && rp.Team == 2 && rp.Race == Commander.Terran && s.Breakpoint == Breakpoint.All
91+
&& su.Unit.Name == "Thor"
9192
select su.Poss;
92-
var posString = query.ToList();
93+
var posString = query
94+
.OrderByDescending(o => o.Length)
95+
.Take(4)
96+
.ToList();
97+
98+
foreach (var pos in posString)
99+
{
100+
Console.WriteLine(pos);
101+
}
93102

94103
var allCoords = new List<(int x, int y)>();
95104

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
using dsstats.shared;
2+
using dsstats.shared.DsFen;
3+
4+
namespace dsstats.builder.tests;
5+
6+
[TestClass]
7+
public sealed class FenSharedTests
8+
{
9+
[TestMethod]
10+
public void FenRoundTrip_FromSpawnDto()
11+
{
12+
var spawn = new SpawnDto
13+
{
14+
Units = new List<SpawnUnitDto>
15+
{
16+
new() { Unit = new() { Name = "Zergling" }, Poss = "91,80" },
17+
new() { Unit = new() { Name = "Mutalisk" }, Poss = "92,81" },
18+
}
19+
};
20+
21+
var cmdr = Commander.Zerg;
22+
int team = 2;
23+
24+
var fen = DsFen.GetFen(spawn, cmdr, team);
25+
26+
var newSpawn = new SpawnDto { Units = [] };
27+
DsFen.ApplyFen(fen, newSpawn, out cmdr, out team);
28+
29+
var newFen = DsFen.GetFen(newSpawn, cmdr, team);
30+
31+
Assert.AreEqual(fen, newFen);
32+
}
33+
34+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
namespace dsstats.builder.tests;
2+
3+
[TestClass]
4+
public sealed class FenTests
5+
{
6+
[TestMethod]
7+
public void CanCreateFen()
8+
{
9+
int team = 2;
10+
var build = CmdrBuildFactory.Create(shared.Commander.Zerg);
11+
Assert.IsNotNull(build);
12+
var possString = "90,79";
13+
var unitName = "Ultralisk";
14+
15+
var buildArea = new BuildArea(team);
16+
buildArea.PlaceUnits(unitName, possString, team);
17+
18+
var fen = buildArea.ToFenString(build);
19+
20+
var fenBuildArea = new BuildArea(team);
21+
fenBuildArea.FromFenString(fen, build);
22+
var reverseFen = fenBuildArea.ToFenString(build);
23+
24+
Assert.AreEqual(fen, reverseFen);
25+
}
26+
27+
[TestMethod]
28+
public void CanCreateFen_WithAirUnit()
29+
{
30+
int team = 2;
31+
var build = CmdrBuildFactory.Create(shared.Commander.Zerg);
32+
Assert.IsNotNull(build);
33+
34+
var buildArea = new BuildArea(team);
35+
buildArea.PlaceUnits("Mutalisk", "91,80", team); // air unit
36+
37+
var fen = buildArea.ToFenString(build);
38+
var fenBuildArea = new BuildArea(team);
39+
fenBuildArea.FromFenString(fen, build);
40+
var reverseFen = fenBuildArea.ToFenString(build);
41+
42+
Assert.AreEqual(fen, reverseFen);
43+
}
44+
45+
[TestMethod]
46+
public void CanCreateFen_WithMultipleUnits()
47+
{
48+
int team = 2;
49+
var build = CmdrBuildFactory.Create(shared.Commander.Zerg);
50+
Assert.IsNotNull(build);
51+
52+
var buildArea = new BuildArea(team);
53+
buildArea.PlaceUnits("Zergling", "91,80", team);
54+
buildArea.PlaceUnits("Queen", "92,80", team);
55+
buildArea.PlaceUnits("Overseer", "91,81", team); // air
56+
57+
var fen = buildArea.ToFenString(build);
58+
var fenBuildArea = new BuildArea(team);
59+
fenBuildArea.FromFenString(fen, build);
60+
var reverseFen = fenBuildArea.ToFenString(build);
61+
62+
Assert.AreEqual(fen, reverseFen);
63+
}
64+
65+
[TestMethod]
66+
public void CanCreateFen_WithToggledUnit()
67+
{
68+
int team = 2;
69+
var build = CmdrBuildFactory.Create(shared.Commander.Zerg);
70+
Assert.IsNotNull(build);
71+
72+
// Assume "Lurker" shares key 'a' with Hydralisk, but Hydralisk is active,
73+
// so Lurker should appear as 'A' (uppercase)
74+
var buildArea = new BuildArea(team);
75+
buildArea.PlaceUnits("Lurker", "93,82", team);
76+
77+
var fen = buildArea.ToFenString(build);
78+
Assert.IsTrue(fen.Contains('A'), $"fen does not contain upper case A: {fen}");
79+
80+
var fenBuildArea = new BuildArea(team);
81+
fenBuildArea.FromFenString(fen, build);
82+
var reverseFen = fenBuildArea.ToFenString(build);
83+
84+
Assert.AreEqual(fen, reverseFen);
85+
}
86+
}

src/dsstats.maui/dsstats.builder/dsstats.builder/BuildArea.cs

Lines changed: 138 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using dsstats.shared.DsFen;
2+
13
namespace dsstats.builder;
24

35
/// <summary>
@@ -50,6 +52,7 @@ public List<InputEvent> GetBuildEvents(ScreenArea screenArea, CmdrBuild build)
5052
List<BuildUnit> topUnits = [];
5153
List<BuildUnit> centerUnits = [];
5254
List<BuildUnit> bottomUnits = [];
55+
WorkerMenu workerMenu = new();
5356

5457
foreach (var unit in allUnits)
5558
{
@@ -71,15 +74,15 @@ public List<InputEvent> GetBuildEvents(ScreenArea screenArea, CmdrBuild build)
7174

7275
foreach (var centerUnit in centerUnits)
7376
{
74-
events.AddRange(build.GetBuildEvents(centerUnit.UnitName, centerUnit.Pos, screenArea));
77+
events.AddRange(build.GetBuildEvents(centerUnit.UnitName, centerUnit.Pos, screenArea, workerMenu));
7578
}
7679
if (topUnits.Count > 0)
7780
{
7881
events.AddRange(DsBuilder.ScrollY(Convert.ToInt32(250 * screenArea._scaleY), screenArea.GetCenter()));
7982
foreach (var topUnit in topUnits)
8083
{
8184
events.AddRange(build.GetBuildEvents(topUnit.UnitName,
82-
topUnit.Pos with { Y = topUnit.Pos.Y + Convert.ToInt32(125 * screenArea._scaleY) }, screenArea));
85+
topUnit.Pos with { Y = topUnit.Pos.Y + Convert.ToInt32(125 * screenArea._scaleY) }, screenArea, workerMenu));
8386
}
8487
}
8588
if (bottomUnits.Count > 0)
@@ -91,7 +94,7 @@ public List<InputEvent> GetBuildEvents(ScreenArea screenArea, CmdrBuild build)
9194
foreach (var bottomUnit in bottomUnits)
9295
{
9396
events.AddRange(build.GetBuildEvents(bottomUnit.UnitName,
94-
bottomUnit.Pos with { Y = bottomUnit.Pos.Y - Convert.ToInt32(300 * screenArea._scaleY) }, screenArea));
97+
bottomUnit.Pos with { Y = bottomUnit.Pos.Y - Convert.ToInt32(300 * screenArea._scaleY) }, screenArea, workerMenu));
9598
}
9699
}
97100

@@ -136,7 +139,10 @@ private List<RlPoint> GetUnitPositions(string unitString, int team)
136139
for (int i = 0; i < stringPoints.Length; i += 2)
137140
{
138141
RlPoint mapPoint = new(int.Parse(stringPoints[i]), int.Parse(stringPoints[i + 1]));
139-
mapPoints.Add(NormalizeToTop(mapPoint));
142+
if (IsPointInPolygon(mapPoint, polygon))
143+
{
144+
mapPoints.Add(NormalizeToTop(mapPoint));
145+
}
140146
}
141147
return mapPoints;
142148
}
@@ -157,6 +163,134 @@ public RlPoint GetCenter()
157163
return new(x1 + ((x2 - x1) / 2), y1 + ((y2 - y1) / 2));
158164
}
159165

166+
public string ToFenString(CmdrBuild build)
167+
{
168+
var groundUnits = new Dictionary<RlPoint, char>();
169+
var airUnits = new Dictionary<RlPoint, char>();
170+
171+
foreach (var kv in units)
172+
{
173+
string unitName = kv.Key;
174+
var buildOption = build.GetUnitBuildOption(unitName);
175+
if (buildOption == null) continue;
176+
177+
char key = buildOption.RequiresToggle && !buildOption.IsActive
178+
? char.ToUpper(buildOption.Key)
179+
: buildOption.Key;
180+
181+
foreach (var pos in kv.Value)
182+
{
183+
if (buildOption.IsAir)
184+
airUnits[pos] = key;
185+
else
186+
groundUnits[pos] = key;
187+
}
188+
}
189+
190+
if (groundUnits.Count == 0 && airUnits.Count == 0)
191+
return "";
192+
193+
// Determine grid bounds (combined for both layers)
194+
var allPoints = groundUnits.Keys.Concat(airUnits.Keys).ToList();
195+
int minX = allPoints.Min(p => p.X);
196+
int maxX = allPoints.Max(p => p.X);
197+
int minY = allPoints.Min(p => p.Y);
198+
int maxY = allPoints.Max(p => p.Y);
199+
200+
string EncodeLayer(Dictionary<RlPoint, char> layer)
201+
{
202+
var rows = new List<string>();
203+
for (int y = minY; y <= maxY; y++)
204+
{
205+
string row = "";
206+
int emptyCount = 0;
207+
208+
for (int x = minX; x <= maxX; x++)
209+
{
210+
var pt = new RlPoint(x, y);
211+
if (layer.TryGetValue(pt, out char key))
212+
{
213+
if (emptyCount > 0)
214+
{
215+
row += emptyCount.ToString();
216+
emptyCount = 0;
217+
}
218+
row += key;
219+
}
220+
else
221+
{
222+
emptyCount++;
223+
}
224+
}
225+
226+
if (emptyCount > 0)
227+
row += emptyCount.ToString();
228+
229+
rows.Add(row);
230+
}
231+
232+
return string.Join("/", rows);
233+
}
234+
235+
var groundFen = EncodeLayer(groundUnits);
236+
var airFen = EncodeLayer(airUnits);
237+
238+
return $"{groundFen}|{airFen}";
239+
}
240+
241+
public void FromFenString(string fen, CmdrBuild build)
242+
{
243+
if (string.IsNullOrWhiteSpace(fen))
244+
return;
245+
246+
// Split into ground and air layers
247+
var layers = fen.Split('|');
248+
string groundLayer = layers.Length > 0 ? layers[0] : "";
249+
string airLayer = layers.Length > 1 ? layers[1] : "";
250+
251+
ParseLayer(groundLayer, build, isAir: false);
252+
if (!string.IsNullOrWhiteSpace(airLayer))
253+
ParseLayer(airLayer, build, isAir: true);
254+
}
255+
256+
private void ParseLayer(string fenLayer, CmdrBuild build, bool isAir)
257+
{
258+
if (string.IsNullOrWhiteSpace(fenLayer))
259+
return;
260+
261+
int y = 0;
262+
var rows = fenLayer.Split('/');
263+
foreach (var row in rows)
264+
{
265+
int x = 0;
266+
foreach (char ch in row)
267+
{
268+
if (char.IsDigit(ch))
269+
{
270+
x += ch - '0';
271+
}
272+
else
273+
{
274+
bool isUpper = char.IsUpper(ch);
275+
char key = char.ToLower(ch);
276+
277+
var unitName = build.GetUnitNameFromKey(key, isAir, isUpper);
278+
if (unitName != null)
279+
{
280+
if (!units.TryGetValue(unitName, out var unitPositions))
281+
unitPositions = units[unitName] = [];
282+
283+
unitPositions.Add(new RlPoint(x, y)); // normalized pos
284+
}
285+
286+
x++;
287+
}
288+
}
289+
290+
y++;
291+
}
292+
}
293+
160294
private bool IsPointInsideOrOnEdge(RlPoint p)
161295
{
162296
if (IsPointInPolygon(p, polygon))

0 commit comments

Comments
 (0)