Skip to content

Commit 9529db6

Browse files
committed
implement a reverse resolver for IDs
1 parent 658db78 commit 9529db6

3 files changed

Lines changed: 145 additions & 0 deletions

File tree

build.sbt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ console / initialCommands := """
1919
import io.github.memo33.metarules.meta._, com.sc4nam.module, module.syntax._
2020
import Implicits._, Network._, Flags._, RotFlip._, Rule.{CopyTile => %}, group.SymGroup._
2121
lazy val resolve = module.Main.resolveSafely
22+
lazy val preimage = module.ReverseResolver.create()
2223
implicit lazy val context: RuleTransducer.Context = RuleTransducer.Context(resolve, module.RegenerateTileOrientationCache.loadCache(), module.MirrorVariants.preprocessor)
2324
def transduce(rule: Rule[SymTile]): Unit = RuleTransducer(rule)(context) foreach println
2425
"""
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package com.sc4nam.module
2+
3+
import io.github.memo33.metarules.meta._, syntax._, Network._, RotFlip._, Flags._
4+
import Implicits.segmentToTile
5+
import NetworkProperties._
6+
7+
/** Resolves from IDs to meta tiles (on a best effort basis).
8+
* This is only intended for diagnosing problems with existing rules, not for
9+
* generating new metarule code.
10+
*/
11+
class ReverseResolver private[module] (val reverseTileMap: collection.Map[Int, ::[Tile]]) extends PartialFunction[IdTile | Int, ::[Tile]] {
12+
private def toId(idTile: IdTile | Int): Int = idTile match {
13+
case x: IdTile => x.id
14+
case id: Int => id
15+
}
16+
def isDefinedAt(idTile: IdTile | Int): Boolean = reverseTileMap.contains(toId(idTile))
17+
def apply(idTile: IdTile | Int): ::[Tile] = {
18+
val fiber = reverseTileMap.apply(toId(idTile))
19+
idTile match {
20+
case x: IdTile => fiber.map(_ * x.rf).asInstanceOf[::[Tile]]
21+
case id: Int => fiber
22+
}
23+
}
24+
}
25+
26+
object ReverseResolver {
27+
28+
/** Checks if tiles define the same tile if segments are merged.
29+
* e.g. (2,0,2,0) & (0,2,0,2) vs (2,2,2,2)
30+
* or (2,0,2,0) & (0,3,0,0) vs (2,13,2,0)
31+
*/
32+
def isSegmentContraction(tile1: Tile, tile2: Tile): Boolean = {
33+
val List(t1, t2) = List(tile1, tile2).sortBy(_.segs.size)
34+
if (t1.segs.size == 1 && t2.segs.size == 2 && t2.segs.forall(_.network == t1.segs.head.network)) {
35+
val s1 = t1.segs.head
36+
val Seq(s2, s3) = t2.segs.toSeq
37+
if ((0 to 3).forall(i => (s2.flags(i) == 0 || s3.flags(i) == 0) && (
38+
s1.flags(i) == s2.flags(i) + s3.flags(i)
39+
|| Seq(s1.flags(i), s2.flags(i) + s3.flags(i)).map(_.abs).forall(f => f == 1 || f == 3 || f == 11 || f == 13)
40+
))) {
41+
true
42+
} else false
43+
} else false
44+
}
45+
46+
/** Checks if tiles are DxD T or + intersections (which might use the same ID)
47+
* e.g. (3,0,0,1) & (0,3,0,0) vs (3,0,0,1) & (1,3,0,0)
48+
*/
49+
def isDiagonalTAmbuigity(tile1: Tile, tile2: Tile): Boolean = {
50+
val diff1 = tile1.segs &~ tile2.segs
51+
val diff2 = tile2.segs &~ tile1.segs
52+
(diff1.toSeq, diff2.toSeq) match {
53+
case (Seq(s1), Seq(s2)) if s1.network == s2.network =>
54+
(0 to 3).filter(i => s1.flags(i) != s2.flags(i)) match {
55+
case Seq(j) =>
56+
(s1.flags(j) == 0 || s2.flags(j) == 0) && {
57+
val f = (s1.flags(j) + s2.flags(j)).abs
58+
(f == 1 || f == 3) && (tile1.segs & tile2.segs).exists(s => s.flags(j).abs + f == 4) // i.e. diagonal and opposing diagonal
59+
}
60+
case _ => false
61+
}
62+
case _ => false
63+
}
64+
}
65+
66+
/** Expand this as necessary to allow multiple metarule definitions to resolve
67+
* to the same ID.
68+
*/
69+
def allowedConflict(idTile: IdTile, tile1: Tile, tile2: Tile): Boolean = {
70+
if (tile1.segs.exists(_.network.isTla) || tile2.segs.exists(_.network.isTla)) {
71+
true // TODO refine
72+
} else if (isSegmentContraction(tile1, tile2)) {
73+
true
74+
} else if (isDiagonalTAmbuigity(tile1, tile2)) {
75+
true
76+
} else if (idTile.id == 0x5D7E0800) { // L1Dtr->L2Dtr upper HT uses same ID as L0Dtr->L2Dtr
77+
true
78+
} else {
79+
false
80+
}
81+
}
82+
83+
def create(): ReverseResolver = {
84+
val maps = Seq(
85+
(new MiscResolver).tileMap,
86+
(new SamResolver).tileMap,
87+
(new RealRailwayResolver).tileMap,
88+
(new RhwResolver).tileMap,
89+
(new flexfly.FlexFlyResolver).tileMap,
90+
(new NwmResolver).tileMap,
91+
)
92+
93+
val reverseTileMap = collection.mutable.Map.empty[Int, ::[Tile]]
94+
val conflicts = collection.mutable.Buffer.empty[(IdTile, Tile, Tile)]
95+
maps.foreach(m => m.iterator.foreach { case (tile, idTile) =>
96+
if (idTile.rf == R0F0) { // we only take and store R0F0 orientations, as tileMaps might not contain all of the other orientations
97+
val tiles = reverseTileMap.get(idTile.id).getOrElse(Nil)
98+
if (!tiles.contains(tile)) {
99+
tiles match {
100+
case (t :: _) if !allowedConflict(idTile, tile, t) =>
101+
conflicts += ((idTile, tile, t))
102+
case _ =>
103+
reverseTileMap(idTile.id) = ::(tile, tiles)
104+
}
105+
} // else nothing to do
106+
}
107+
})
108+
109+
if (conflicts.nonEmpty) {
110+
for ((idTile, tile1, tile2) <- conflicts) {
111+
if (!allowedConflict(idTile, tile1, tile2)) {
112+
println(s"Conflicting metarule-resolution for ID $idTile: $tile1 vs $tile2")
113+
}
114+
}
115+
throw new AssertionError(s"There were ${conflicts.length} conflicting metarule-resolutions for IDs.")
116+
}
117+
new ReverseResolver(reverseTileMap)
118+
}
119+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.sc4nam.module
2+
3+
import org.scalatest.wordspec.AnyWordSpec
4+
import org.scalatest.matchers.should.Matchers
5+
import io.github.memo33.metarules.meta._
6+
import syntax._, Implicits._, RotFlip._, Network._, Flags._, NetworkProperties._
7+
8+
class ReverseResolverSpec extends AnyWordSpec with Matchers {
9+
10+
"ReverseResolver" should {
11+
"find all possible orientations of a tile" in {
12+
val preimage = new ReverseResolver(Map((0x00004b00, ::(Road~NS, Nil))))
13+
for (rf <- RotFlip.values) {
14+
preimage(IdTile(0x00004b00, rf)).shouldBe(Seq[Tile](Road~NS * rf))
15+
}
16+
}
17+
"find multiple inequivalent tiles if they map to the same ID" in {
18+
val t1: Tile = Street~(2,2,2,2)
19+
val t2: Tile = Street~NS & Street~EW
20+
val preimage = new ReverseResolver(Map((0x05020700, ::(t1, t2 :: Nil))))
21+
preimage(0x05020700).toSet.shouldBe(Set(t1, t2))
22+
preimage(IdTile(0x05020700,0,0)).toSet.shouldBe(Set(t1, t2))
23+
}
24+
}
25+
}

0 commit comments

Comments
 (0)