Support relaxed end points in TilingPath

Whilst TilingPath has always allowed deviating from the path points
between the start and end point, it didn't allow deviation from the
start or end points themselves. This change allows the end point to
deviate if the tiling would otherwise fail. The start point remains
strictly positioned, as before. Loop end points (which must necessarily
match the start) also remain strict.

The ExperimentalMapGenerator is modified to benefit from the relaxed
tiling constraints. As a result, failed map generation due to tiling
failures is much rarer, and will make some otherwise very inflexible
template categories viable for tiling.

Includes some impure refactoring around the treatment of start,
intermediate, and end segments. As such, this changes map generation
output, even for maps which already tiled perfectly.

A previous workaround used for CnC road templates, whereby additional
segments were defined for road ending templates, is now redundant and
cleaned up.
This commit is contained in:
Ashley Newson
2025-04-06 23:24:42 +01:00
committed by Gustas Kažukauskas
parent b2acc653a0
commit f09959ecba
7 changed files with 204 additions and 226 deletions

View File

@@ -15,6 +15,7 @@ using System.Collections.Immutable;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using OpenRA.Primitives; using OpenRA.Primitives;
using OpenRA.Support;
namespace OpenRA.Mods.Common.MapGenerator namespace OpenRA.Mods.Common.MapGenerator
{ {
@@ -1502,5 +1503,34 @@ namespace OpenRA.Mods.Common.MapGenerator
return matrix; return matrix;
} }
/// <summary>
/// Rank all cell values and select the best (greatest compared) value.
/// If there are equally good best candidates, choose one at random.
/// </summary>
public static (int2 MPos, T Value) FindRandomBest<T>(
Matrix<T> matrix,
MersenneTwister random,
Comparison<T> comparison)
{
var candidates = new List<int2>();
var best = matrix[new int2(0, 0)];
for (var y = 0; y < matrix.Size.Y; y++)
for (var x = 0; x < matrix.Size.X; x++)
{
var rank = comparison(matrix[x, y], best);
if (rank > 0)
{
best = matrix[x, y];
candidates.Clear();
}
if (rank >= 0)
candidates.Add(new int2(x, y));
}
var choice = candidates[random.Next(candidates.Count)];
return (choice, best);
}
} }
} }

View File

@@ -212,6 +212,13 @@ namespace OpenRA.Mods.Common.MapGenerator
/// </summary> /// </summary>
public int MinSeparation; public int MinSeparation;
/// <summary>
/// If the path cannot be tiled exactly, the resulting tiling is allowed to deviate from
/// target end point by this Chebychev distance. Ignored for loops. This will be capped to
/// MaxDeviation at tiling time.
/// </summary>
public int MaxEndDeviation;
/// <summary> /// <summary>
/// Stores start type and direction. /// Stores start type and direction.
/// </summary> /// </summary>
@@ -242,6 +249,7 @@ namespace OpenRA.Mods.Common.MapGenerator
MaxDeviation = maxDeviation; MaxDeviation = maxDeviation;
MaxSkip = 0; MaxSkip = 0;
MinSeparation = 0; MinSeparation = 0;
MaxEndDeviation = 0;
Start = new Terminal(startType, null); Start = new Terminal(startType, null);
End = new Terminal(endType, null); End = new Terminal(endType, null);
Segments = permittedTemplates; Segments = permittedTemplates;
@@ -337,6 +345,9 @@ namespace OpenRA.Mods.Common.MapGenerator
// The search is conducted from the path start node until the best possible cost of // The search is conducted from the path start node until the best possible cost of
// the end node is confirmed. This also populates possible intermediate nodes' costs. // the end node is confirmed. This also populates possible intermediate nodes' costs.
// //
// If the original target end node is unreachable (at MaxCost), a nearby node may be
// selected as a fallback end point, provided the target path isn't a loop.
//
// Then, from the end node, it works backwards. It finds any (random) suitable template // Then, from the end node, it works backwards. It finds any (random) suitable template
// segment which connects back to a previous node where the difference in cost is // segment which connects back to a previous node where the difference in cost is
// that of the template segment's cost, implying that the previous node is on an // that of the template segment's cost, implying that the previous node is on an
@@ -563,12 +574,25 @@ namespace OpenRA.Mods.Common.MapGenerator
var pathEnd = points[^1]; var pathEnd = points[^1];
var orderedPermittedSegments = Segments.All.ToImmutableArray(); var orderedPermittedSegments = Segments.All.ToImmutableArray();
var permittedSegments = orderedPermittedSegments.ToImmutableHashSet(); var permittedSegments = orderedPermittedSegments.ToImmutableHashSet();
var permittedStartSegments = Segments.Start.ToImmutableHashSet();
var permittedInnerSegments = Segments.Inner.ToImmutableHashSet();
var permittedEndSegments = Segments.End.ToImmutableHashSet();
const int MaxCost = int.MaxValue; const int MaxCost = int.MaxValue;
var segmentTypeToId = new Dictionary<string, int>(); var segmentTypeToId = new Dictionary<string, int>();
var segmentsByStart = new List<List<TilingSegment>>(); var segmentsByStart = new List<List<(TilingSegment Segment, bool CanStart, bool CanInner, bool CanEnd)>>();
var segmentsByEnd = new List<List<TilingSegment>>(); var segmentsByEnd = new List<List<(TilingSegment Segment, bool CanStart, bool CanInner, bool CanEnd)>>();
var costs = new List<Matrix<int>>();
// We store the end costs of valid end segments separately to inner costs.
//
// Note also that:
// - The start cost is always zero and only applies to a single node.
// - Permitted end and inner segments may be distinct, but the end terminal could exist
// in the permitted inner segments and shouldn't be a valid intermediate cost.
// - Avoids confusing start, inner, and end costs when processing looped paths.
// - We may be interested in multiple end costs if MaxEndDeviation is non-zero.
var endCosts = new Matrix<int>(size).Fill(MaxCost);
var innerCosts = new List<Matrix<int>>();
{ {
void RegisterSegmentType(string type) void RegisterSegmentType(string type)
{ {
@@ -578,7 +602,7 @@ namespace OpenRA.Mods.Common.MapGenerator
segmentTypeToId.Add(type, newId); segmentTypeToId.Add(type, newId);
segmentsByStart.Add([]); segmentsByStart.Add([]);
segmentsByEnd.Add([]); segmentsByEnd.Add([]);
costs.Add(new Matrix<int>(size).Fill(MaxCost)); innerCosts.Add(new Matrix<int>(size).Fill(MaxCost));
} }
foreach (var segment in orderedPermittedSegments) foreach (var segment in orderedPermittedSegments)
@@ -589,8 +613,13 @@ namespace OpenRA.Mods.Common.MapGenerator
var startTypeId = segmentTypeToId[segment.Start]; var startTypeId = segmentTypeToId[segment.Start];
var endTypeId = segmentTypeToId[segment.End]; var endTypeId = segmentTypeToId[segment.End];
var tilePathSegment = new TilingSegment(template, segment, startTypeId, endTypeId); var tilePathSegment = new TilingSegment(template, segment, startTypeId, endTypeId);
segmentsByStart[startTypeId].Add(tilePathSegment); var tuple = (
segmentsByEnd[endTypeId].Add(tilePathSegment); tilePathSegment,
permittedStartSegments.Contains(segment) && segment.Start == start.SegmentType,
permittedInnerSegments.Contains(segment),
permittedEndSegments.Contains(segment) && segment.End == end.SegmentType);
segmentsByStart[startTypeId].Add(tuple);
segmentsByEnd[endTypeId].Add(tuple);
} }
} }
@@ -610,43 +639,18 @@ namespace OpenRA.Mods.Common.MapGenerator
var pathStartTypeId = segmentTypeToId[start.SegmentType]; var pathStartTypeId = segmentTypeToId[start.SegmentType];
var pathEndTypeId = segmentTypeToId[end.SegmentType]; var pathEndTypeId = segmentTypeToId[end.SegmentType];
var innerTypeIds = Segments.Inner
.SelectMany(segment => new[] { segment.Start, segment.End })
.Select(segmentType => segmentTypeToId[segmentType])
.ToImmutableHashSet();
// Lower (closer to zero) costs are better matches. // Lower (closer to zero) costs are better matches.
// MaxScore means totally unacceptable. // MaxScore means totally unacceptable.
int ScoreSegment(TilingSegment segment, CVec from) int ScoreSegment(TilingSegment segment, CVec from)
{ {
if (from == pathStart)
{
if (segment.StartTypeId != pathStartTypeId)
return MaxCost;
}
else
{
if (!innerTypeIds.Contains(segment.StartTypeId))
return MaxCost;
}
var to = from + segment.Moves; var to = from + segment.Moves;
if (to == pathEnd)
{
if (segment.EndTypeId != pathEndTypeId)
return MaxCost;
}
else
{
if (!innerTypeIds.Contains(segment.EndTypeId))
return MaxCost;
if (isLoop && lowProgress[from.X, from.Y] > highProgress[to.X, to.Y] && highProgress[to.X, to.Y] != 0) if (isLoop && to != pathEnd && lowProgress[from.X, from.Y] > highProgress[to.X, to.Y] && highProgress[to.X, to.Y] != 0)
{ {
// We've missed the start/end of the loop and have potentially gone past it // We've missed the start/end of the loop and have potentially gone past it
// (as far as low and high progress are concerned). // (as far as low and high progress are concerned).
return MaxCost; return MaxCost;
}
} }
var deviationAcc = 0; var deviationAcc = 0;
@@ -700,10 +704,23 @@ namespace OpenRA.Mods.Common.MapGenerator
return deviationAcc; return deviationAcc;
} }
void UpdateFrom(CVec from, int fromTypeId, int fromCost) void UpdateFrom(CVec from, int fromTypeId, bool isForStart)
{ {
foreach (var segment in segmentsByStart[fromTypeId]) var fromCost = isForStart ? 0 : innerCosts[fromTypeId][from.X, from.Y];
foreach (var (segment, canStart, canInner, canEnd) in segmentsByStart[fromTypeId])
{ {
if (isForStart)
{
if (!canStart)
continue;
}
else
{
if (!(canEnd || canInner))
continue;
}
var to = from + segment.Moves; var to = from + segment.Moves;
if (to.X < 0 || to.X >= size.X || to.Y < 0 || to.Y >= size.Y) if (to.X < 0 || to.X >= size.X || to.Y < 0 || to.Y >= size.Y)
continue; continue;
@@ -721,44 +738,59 @@ namespace OpenRA.Mods.Common.MapGenerator
var toCost = fromCost + segmentCost; var toCost = fromCost + segmentCost;
var toTypeId = segment.EndTypeId; var toTypeId = segment.EndTypeId;
if (toCost < costs[toTypeId][to.X, to.Y])
if ((canStart || canInner) && toCost < innerCosts[toTypeId][to.X, to.Y])
{ {
costs[toTypeId][to.X, to.Y] = toCost; innerCosts[toTypeId][to.X, to.Y] = toCost;
SetPriorityAt(toTypeId, to, toCost); SetPriorityAt(toTypeId, to, toCost);
} }
if (canEnd && toCost < endCosts[to.X, to.Y])
endCosts[to.X, to.Y] = toCost;
} }
SetPriorityAt(fromTypeId, from, MaxCost); SetPriorityAt(fromTypeId, from, MaxCost);
} }
// costs[pathStartTypeId][pathStart.X, pathStart.Y] is preset to UpdateFrom(pathStart, pathStartTypeId, true);
// MaxCost, but we pass in a cost of 0 for the first iteration. We
// leave it like this in case this is a looped path with a shared
// start and end point. We set it to 0 later when tracing back.
UpdateFrom(pathStart, pathStartTypeId, 0);
while (true) while (true)
{ {
var (fromTypeId, from, priority) = GetNextPriority(); var (fromTypeId, from, priority) = GetNextPriority();
if (priority == MaxCost || from == pathEnd) if (priority == MaxCost)
break; break;
UpdateFrom(from, fromTypeId, costs[fromTypeId][from.X, from.Y]); UpdateFrom(from, fromTypeId, false);
} }
// Trace back and update tiles // Trace back and update tiles
var resultPoints = new List<CPos> var resultPoints = new List<CPos>();
{
new(pathEnd.X + minPoint.X, pathEnd.Y + minPoint.Y)
};
(CVec From, int FromTypeId) TraceBackStep(CVec to, int toTypeId, int toCost) (CVec From, int FromTypeId) TraceBackStep(CVec to, int toTypeId, bool isForEnd)
{ {
var toCost = isForEnd ? endCosts[to.X, to.Y] : innerCosts[toTypeId][to.X, to.Y];
var candidates = new List<TilingSegment>(); var candidates = new List<TilingSegment>();
foreach (var segment in segmentsByEnd[toTypeId]) foreach (var (segment, canStart, canInner, canEnd) in segmentsByEnd[toTypeId])
{ {
if (isForEnd)
{
if (!canEnd)
continue;
}
else
{
if (!(canStart || canInner))
continue;
}
var from = to - segment.Moves; var from = to - segment.Moves;
var mustStart =
from == pathStart && segment.StartTypeId == pathStartTypeId;
if (mustStart && !canStart)
continue;
if (from.X < 0 || from.X >= size.X || from.Y < 0 || from.Y >= size.Y) if (from.X < 0 || from.X >= size.X || from.Y < 0 || from.Y >= size.Y)
continue; continue;
@@ -774,7 +806,9 @@ namespace OpenRA.Mods.Common.MapGenerator
continue; continue;
var fromCost = toCost - segmentCost; var fromCost = toCost - segmentCost;
if (fromCost == costs[segment.StartTypeId][from.X, from.Y]) var requiredFromCost =
mustStart ? 0 : innerCosts[segment.StartTypeId][from.X, from.Y];
if (fromCost == requiredFromCost)
candidates.Add(segment); candidates.Add(segment);
} }
@@ -794,24 +828,80 @@ namespace OpenRA.Mods.Common.MapGenerator
} }
{ {
var to = pathEnd;
var toTypeId = pathEndTypeId; var toTypeId = pathEndTypeId;
var bestCost = costs[toTypeId][to.X, to.Y];
if (bestCost == MaxCost)
return null;
// For non-loops, this remained unset at MaxCost. For loops, if (endCosts[pathEnd.X, pathEnd.Y] == MaxCost)
// this was the shared start and end point and got set to {
// bestCost. We set it to 0 for traceback, but perform the // There isn't a tiling solution to the exact target end point. If enabled,
// first iteration using bestCost. (The opposite of how we // search for an alternative, nearby end point.
// traced forward.) var maxEndDeviation = Math.Min(MaxEndDeviation, MaxDeviation);
costs[pathStartTypeId][pathStart.X, pathStart.Y] = 0; if (maxEndDeviation == 0 || isLoop)
return null;
(to, toTypeId) = TraceBackStep(to, toTypeId, bestCost); // Find the closest points which are near the original target end point and
// have a tiling solution.
const int Unreached = int.MaxValue;
const int Unsolved = int.MaxValue - 1;
var fallbackDistances =
new Matrix<int>(maxEndDeviation * 2 + 1, maxEndDeviation * 2 + 1)
.Fill(Unreached);
int? FallbacksFiller(int2 xy, int distance)
{
if (fallbackDistances[xy] != Unreached)
return null;
var p = new int2(pathEnd.X - maxEndDeviation, pathEnd.Y - maxEndDeviation) + xy;
if (!deviations.ContainsXY(p.X, p.Y) || deviations[p.X, p.Y] == OverDeviation)
{
fallbackDistances[xy] = Unsolved;
return null;
}
fallbackDistances[xy] =
endCosts[p.X, p.Y] != MaxCost ? distance : Unsolved;
return distance + 1;
}
MatrixUtils.FloodFill(
fallbackDistances.Size,
[(new int2(maxEndDeviation, maxEndDeviation), 0)],
FallbacksFiller,
Direction.Spread4);
var bestDistance = fallbackDistances.Data.Min();
if (bestDistance == Unreached || bestDistance == Unsolved)
return null;
// Find the lowest cost candidate end point.
var fallbackCosts = new Matrix<int>(maxEndDeviation * 2 + 1, maxEndDeviation * 2 + 1);
for (var y = -maxEndDeviation; y <= maxEndDeviation; y++)
for (var x = -maxEndDeviation; x <= maxEndDeviation; x++)
{
var fallbackXy = new int2(x + maxEndDeviation, y + maxEndDeviation);
var p = new int2(x + pathEnd.X, y + pathEnd.Y);
fallbackCosts[fallbackXy] =
(fallbackDistances[fallbackXy] == bestDistance) ? endCosts[p] : MaxCost;
}
var (chosenXy, _) = MatrixUtils.FindRandomBest(
fallbackCosts,
random,
(a, b) => b.CompareTo(a));
pathEnd = new CVec(chosenXy.X - maxEndDeviation, chosenXy.Y - maxEndDeviation) + pathEnd;
}
var to = pathEnd;
resultPoints.Add(new(to.X + minPoint.X, to.Y + minPoint.Y));
(to, toTypeId) = TraceBackStep(to, toTypeId, true);
// No need to check direction. If that is an issue, I have bigger problems to worry about. // No need to check direction. If that is an issue, I have bigger problems to worry about.
while (to != pathStart) while (to != pathStart)
(to, toTypeId) = TraceBackStep(to, toTypeId, costs[toTypeId][to.X, to.Y]); (to, toTypeId) = TraceBackStep(to, toTypeId, false);
} }
// Traced back in reverse, so reverse the reversal. // Traced back in reverse, so reverse the reversal.
@@ -1275,5 +1365,19 @@ namespace OpenRA.Mods.Common.MapGenerator
return true; return true;
} }
/// <summary>Set MaxEndDeviation.</summary>
public TilingPath SetMaxEndDeviation(int maxEndDeviation)
{
MaxEndDeviation = maxEndDeviation;
return this;
}
/// <summary>Allow end point deviation as far as MaxDeviation will allow.</summary>
public TilingPath SetAutoEndDeviation()
{
MaxEndDeviation = int.MaxValue;
return this;
}
} }
} }

View File

@@ -666,6 +666,7 @@ namespace OpenRA.Mods.Common.Traits
beachPermittedTemplates); beachPermittedTemplates);
beachPath beachPath
.ExtendEdge(4) .ExtendEdge(4)
.SetAutoEndDeviation()
.OptimizeLoop(); .OptimizeLoop();
tiledBeaches[i] = tiledBeaches[i] =
beachPath.Tile(beachTilingRandom) beachPath.Tile(beachTilingRandom)
@@ -732,6 +733,7 @@ namespace OpenRA.Mods.Common.Traits
nonLoopedCliffPermittedTemplates); nonLoopedCliffPermittedTemplates);
cliffPath cliffPath
.ExtendEdge(4) .ExtendEdge(4)
.SetAutoEndDeviation()
.OptimizeLoop(); .OptimizeLoop();
if (cliffPath.Tile(cliffTilingRandom) == null) if (cliffPath.Tile(cliffTilingRandom) == null)
throw new MapGenerationException("Could not fit tiles for exterior circle cliffs"); throw new MapGenerationException("Could not fit tiles for exterior circle cliffs");
@@ -812,6 +814,7 @@ namespace OpenRA.Mods.Common.Traits
nonLoopedCliffPermittedTemplates); nonLoopedCliffPermittedTemplates);
cliffPath cliffPath
.ExtendEdge(4) .ExtendEdge(4)
.SetAutoEndDeviation()
.OptimizeLoop(); .OptimizeLoop();
if (cliffPath.Tile(cliffTilingRandom) == null) if (cliffPath.Tile(cliffTilingRandom) == null)
throw new MapGenerationException("Could not fit tiles for cliffs"); throw new MapGenerationException("Could not fit tiles for cliffs");
@@ -1176,6 +1179,7 @@ namespace OpenRA.Mods.Common.Traits
.ExtendEdge(2 * roadTotalShrink + RoadMinimumShrinkLength) .ExtendEdge(2 * roadTotalShrink + RoadMinimumShrinkLength)
.Shrink(roadTotalShrink, RoadMinimumShrinkLength) .Shrink(roadTotalShrink, RoadMinimumShrinkLength)
.InertiallyExtend(RoadStraightenGrow, RoadInertialRange) .InertiallyExtend(RoadStraightenGrow, RoadInertialRange)
.SetAutoEndDeviation()
.OptimizeLoop() .OptimizeLoop()
.RetainIfValid(); .RetainIfValid();

View File

@@ -1069,16 +1069,6 @@ Templates:
Inner: Road Inner: Road
Points: 2,1, 2,0 Points: 2,1, 2,0
Start: Clear.U Start: Clear.U
Segment@1extended:
End: Clear.D
Inner: Road
Points: 1,0, 1,1, 1,2
Start: Road.D
Segment@2extended:
End: Road.U
Inner: Road
Points: 2,2, 2,1, 2,0
Start: Clear.U
Template@94: Template@94:
Id: 94 Id: 94
Images: d02.des Images: d02.des
@@ -1100,16 +1090,6 @@ Templates:
Inner: Road Inner: Road
Points: 1,1, 2,1 Points: 1,1, 2,1
Start: Clear.R Start: Clear.R
Segment@1extended:
End: Clear.L
Inner: Road
Points: 2,0, 1,0, 0,0
Start: Road.L
Segment@2extended:
End: Road.R
Inner: Road
Points: 0,1, 1,1, 2,1
Start: Clear.R
Template@95: Template@95:
Id: 95 Id: 95
Images: d03.des Images: d03.des
@@ -1129,16 +1109,6 @@ Templates:
Inner: Road Inner: Road
Points: 0,1, 0,2 Points: 0,1, 0,2
Start: Clear.D Start: Clear.D
Segment@1extended:
End: Clear.U
Inner: Road
Points: 1,2, 1,1, 1,0
Start: Road.U
Segment@2extended:
End: Road.D
Inner: Road
Points: 0,0, 0,1, 0,2
Start: Clear.D
Template@96: Template@96:
Id: 96 Id: 96
Images: d04.des Images: d04.des
@@ -1159,16 +1129,6 @@ Templates:
Inner: Road Inner: Road
Points: 1,1, 0,1 Points: 1,1, 0,1
Start: Clear.L Start: Clear.L
Segment@1extended:
End: Clear.R
Inner: Road
Points: 0,2, 1,2, 2,2
Start: Road.R
Segment@2extended:
End: Road.L
Inner: Road
Points: 2,1, 1,1, 0,1
Start: Clear.L
Template@97: Template@97:
Id: 97 Id: 97
Images: d05.des Images: d05.des

View File

@@ -1143,16 +1143,6 @@ Templates:
Inner: Road Inner: Road
Points: 2,1, 2,0 Points: 2,1, 2,0
Start: Clear.U Start: Clear.U
Segment@1extended:
End: Clear.D
Inner: Road
Points: 1,0, 1,1, 1,2
Start: Road.D
Segment@2extended:
End: Road.U
Inner: Road
Points: 2,2, 2,1, 2,0
Start: Clear.U
Template@94: Template@94:
Id: 94 Id: 94
Images: d02.sno Images: d02.sno
@@ -1174,16 +1164,6 @@ Templates:
Inner: Road Inner: Road
Points: 1,1, 2,1 Points: 1,1, 2,1
Start: Clear.R Start: Clear.R
Segment@1extended:
End: Clear.L
Inner: Road
Points: 2,0, 1,0, 0,0
Start: Road.L
Segment@2extended:
End: Road.R
Inner: Road
Points: 0,1, 1,1, 2,1
Start: Clear.R
Template@95: Template@95:
Id: 95 Id: 95
Images: d03.sno Images: d03.sno
@@ -1203,16 +1183,6 @@ Templates:
Inner: Road Inner: Road
Points: 0,1, 0,2 Points: 0,1, 0,2
Start: Clear.D Start: Clear.D
Segment@1extended:
End: Clear.U
Inner: Road
Points: 1,2, 1,1, 1,0
Start: Road.U
Segment@2extended:
End: Road.D
Inner: Road
Points: 0,0, 0,1, 0,2
Start: Clear.D
Template@96: Template@96:
Id: 96 Id: 96
Images: d04.sno Images: d04.sno
@@ -1233,16 +1203,6 @@ Templates:
Inner: Road Inner: Road
Points: 1,1, 0,1 Points: 1,1, 0,1
Start: Clear.L Start: Clear.L
Segment@1extended:
End: Clear.R
Inner: Road
Points: 0,2, 1,2, 2,2
Start: Road.R
Segment@2extended:
End: Road.L
Inner: Road
Points: 2,1, 1,1, 0,1
Start: Clear.L
Template@97: Template@97:
Id: 97 Id: 97
Images: d05.sno Images: d05.sno

View File

@@ -1131,16 +1131,6 @@ Templates:
Inner: Road Inner: Road
Points: 2,1, 2,0 Points: 2,1, 2,0
Start: Clear.U Start: Clear.U
Segment@1extended:
End: Clear.D
Inner: Road
Points: 1,0, 1,1, 1,2
Start: Road.D
Segment@2extended:
End: Road.U
Inner: Road
Points: 2,2, 2,1, 2,0
Start: Clear.U
Template@94: Template@94:
Id: 94 Id: 94
Images: d02.tem Images: d02.tem
@@ -1162,16 +1152,6 @@ Templates:
Inner: Road Inner: Road
Points: 1,1, 2,1 Points: 1,1, 2,1
Start: Clear.R Start: Clear.R
Segment@1extended:
End: Clear.L
Inner: Road
Points: 2,0, 1,0, 0,0
Start: Road.L
Segment@2extended:
End: Road.R
Inner: Road
Points: 0,1, 1,1, 2,1
Start: Clear.R
Template@95: Template@95:
Id: 95 Id: 95
Images: d03.tem Images: d03.tem
@@ -1191,16 +1171,6 @@ Templates:
Inner: Road Inner: Road
Points: 0,1, 0,2 Points: 0,1, 0,2
Start: Clear.D Start: Clear.D
Segment@1extended:
End: Clear.U
Inner: Road
Points: 1,2, 1,1, 1,0
Start: Road.U
Segment@2extended:
End: Road.D
Inner: Road
Points: 0,0, 0,1, 0,2
Start: Clear.D
Template@96: Template@96:
Id: 96 Id: 96
Images: d04.tem Images: d04.tem
@@ -1221,16 +1191,6 @@ Templates:
Inner: Road Inner: Road
Points: 1,1, 0,1 Points: 1,1, 0,1
Start: Clear.L Start: Clear.L
Segment@1extended:
End: Clear.R
Inner: Road
Points: 0,2, 1,2, 2,2
Start: Road.R
Segment@2extended:
End: Road.L
Inner: Road
Points: 2,1, 1,1, 0,1
Start: Clear.L
Template@97: Template@97:
Id: 97 Id: 97
Images: d05.tem Images: d05.tem

View File

@@ -1115,16 +1115,6 @@ Templates:
Inner: Road Inner: Road
Points: 2,1, 2,0 Points: 2,1, 2,0
Start: Clear.U Start: Clear.U
Segment@1extended:
End: Clear.D
Inner: Road
Points: 1,0, 1,1, 1,2
Start: Road.D
Segment@2extended:
End: Road.U
Inner: Road
Points: 2,2, 2,1, 2,0
Start: Clear.U
Template@94: Template@94:
Id: 94 Id: 94
Images: d02.win Images: d02.win
@@ -1146,16 +1136,6 @@ Templates:
Inner: Road Inner: Road
Points: 1,1, 2,1 Points: 1,1, 2,1
Start: Clear.R Start: Clear.R
Segment@1extended:
End: Clear.L
Inner: Road
Points: 2,0, 1,0, 0,0
Start: Road.L
Segment@2extended:
End: Road.R
Inner: Road
Points: 0,1, 1,1, 2,1
Start: Clear.R
Template@95: Template@95:
Id: 95 Id: 95
Images: d03.win Images: d03.win
@@ -1175,16 +1155,6 @@ Templates:
Inner: Road Inner: Road
Points: 0,1, 0,2 Points: 0,1, 0,2
Start: Clear.D Start: Clear.D
Segment@1extended:
End: Clear.U
Inner: Road
Points: 1,2, 1,1, 1,0
Start: Road.U
Segment@2extended:
End: Road.D
Inner: Road
Points: 0,0, 0,1, 0,2
Start: Clear.D
Template@96: Template@96:
Id: 96 Id: 96
Images: d04.win Images: d04.win
@@ -1205,16 +1175,6 @@ Templates:
Inner: Road Inner: Road
Points: 1,1, 0,1 Points: 1,1, 0,1
Start: Clear.L Start: Clear.L
Segment@1extended:
End: Clear.R
Inner: Road
Points: 0,2, 1,2, 2,2
Start: Road.R
Segment@2extended:
End: Road.L
Inner: Road
Points: 2,1, 1,1, 0,1
Start: Clear.L
Template@97: Template@97:
Id: 97 Id: 97
Images: d05.win Images: d05.win