Skip to content

Commit 328bb2e

Browse files
committed
Merge branch 'redundant-adjacencies-checker' into redundant-adjacencies-removal
2 parents 0671b00 + e4200b1 commit 328bb2e

3 files changed

Lines changed: 157 additions & 104 deletions

File tree

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package com.sc4nam.module
2+
3+
import java.nio.file.{Files, Paths, Path}
4+
import io.github.memo33.metarules.meta.{RotFlip, EquivRule, Rule, IdTile}
5+
import RotFlip._
6+
import syntax.IdTile
7+
8+
object Rul2Model {
9+
10+
sealed trait Driveside
11+
case object Rhd extends Driveside
12+
case object Lhd extends Driveside
13+
case object RhdAndLhd extends Driveside
14+
15+
def isRulFile(path: Path) = {
16+
val s = path.getFileName().toString()
17+
s.endsWith(".txt") || s.endsWith(".rul")
18+
}
19+
20+
/** Iterate all RUL files in a directory tree, following the order of the NAMControllerCompiler,
21+
* see https://github.com/memo33/NAMControllerCompiler/blob/4c375beaacf2d69aee96d923d364e47128b09fd3/src/controller/tasks/CollectRULsTask.java#L60
22+
*/
23+
def iterateRulFiles(directory: Path): Iterator[Path] = {
24+
import scala.jdk.StreamConverters._
25+
val children: Seq[Path] = Files.list(directory).toScala(Seq).sorted
26+
children.iterator.flatMap { child =>
27+
if (Files.isDirectory(child))
28+
iterateRulFiles(child)
29+
else if (isRulFile(child))
30+
Iterator(child)
31+
else
32+
Iterator.empty
33+
}
34+
}
35+
36+
def drivesideOfFile(path: Path): Driveside = {
37+
val name = path.getFileName().toString()
38+
if (name.contains("rhd.")) Rhd
39+
else if (name.contains("lhd.")) Lhd
40+
else RhdAndLhd
41+
}
42+
43+
def parseRule(line: String): Option[Rule[IdTile]] = {
44+
val chunk = line.split(";|\\[", 2)(0).trim
45+
if (chunk.isEmpty) {
46+
None
47+
} else try {
48+
val ts = chunk.split(",|=").grouped(3).toSeq.map(tup =>
49+
IdTile(java.lang.Long.decode(tup(0)).toInt, RotFlip(tup(1).toInt, tup(2).toInt)))
50+
Some(Rule(ts(0), ts(1), ts(2), ts(3)))
51+
} catch {
52+
case _: IllegalArgumentException => // syntax errors in RUL2 code
53+
// throw new IllegalArgumentException(line)
54+
None
55+
}
56+
}
57+
58+
val lhdPrefix = ";###LHD###"
59+
val rhdPrefix = ";###RHD###"
60+
61+
def parseRuleWithRestrictedDriveside(line: String, driveside: Driveside): Option[(Rule[IdTile], Driveside)] = {
62+
val line1 = line.trim()
63+
if (line1.startsWith(lhdPrefix)) {
64+
if (driveside == Rhd) None else parseRule(line1.substring(lhdPrefix.length)).map(_ -> Lhd)
65+
} else if (line1.startsWith(rhdPrefix)) {
66+
if (driveside == Lhd) None else parseRule(line1.substring(rhdPrefix.length)).map(_ -> Rhd)
67+
} else {
68+
parseRule(line1).map(_ -> driveside)
69+
}
70+
}
71+
72+
def applyRule(rule: Rule[IdTile], t0: IdTile, t1: IdTile): Option[(IdTile, IdTile)] = {
73+
if (t0 == rule(0) && t1 == rule(1)) Some((rule(2), rule(3)))
74+
else if (t0 == rule(0) * R2F1 && t1 == rule(1) * R2F1) Some((rule(2) * R2F1, rule(3) * R2F1))
75+
else if (t0 == rule(1) * R0F1 && t1 == rule(0) * R0F1) Some((rule(3) * R0F1, rule(2) * R0F1))
76+
else if (t0 == rule(1) * R2F0 && t1 == rule(0) * R2F0) Some((rule(3) * R2F0, rule(2) * R2F0))
77+
else None
78+
}
79+
80+
def load(directory: Path): Rul2Model = {
81+
val rulesRhd = collection.mutable.Map.empty[EquivRule, Rule[IdTile]]
82+
val rulesLhd = collection.mutable.Map.empty[EquivRule, Rule[IdTile]]
83+
val rulesShared = collection.mutable.Map.empty[EquivRule, Rule[IdTile]]
84+
val lookupRuleRhd: PartialFunction[EquivRule, Rule[IdTile]] = rulesShared.orElse(rulesRhd) // the two maps should be disjoint
85+
val lookupRuleLhd: PartialFunction[EquivRule, Rule[IdTile]] = rulesShared.orElse(rulesLhd) // the two maps should be disjoint
86+
87+
LOGGER.info(s"""Loading all RUL2 code for RHD and LHD from "$directory"""")
88+
iterateRulFiles(directory).foreach { path =>
89+
val drivesideFile = drivesideOfFile(path)
90+
scala.util.Using.resource(new java.util.Scanner(path.toFile(), "UTF-8")) { scanner =>
91+
while(scanner.hasNextLine()) {
92+
parseRuleWithRestrictedDriveside(scanner.nextLine(), drivesideFile) match {
93+
// only the first matching rule is loaded by the game, so we store only the first one read
94+
case Some((rule, Rhd)) =>
95+
val key = new EquivRule(rule)
96+
if (!rulesShared.contains(key) && !rulesRhd.contains(key))
97+
rulesRhd.addOne(key, rule)
98+
case Some((rule, Lhd)) =>
99+
val key = new EquivRule(rule)
100+
if (!rulesShared.contains(key) && !rulesLhd.contains(key))
101+
rulesLhd.addOne(key, rule)
102+
case Some((rule, RhdAndLhd)) =>
103+
val key = new EquivRule(rule)
104+
if (!rulesShared.contains(key)) {
105+
if (!rulesRhd.contains(key) && !rulesLhd.contains(key))
106+
rulesShared.addOne(key, rule)
107+
else
108+
require(
109+
rulesRhd.contains(key) == rulesLhd.contains(key),
110+
s"There's an unexpected rule conflict that differs between RHD and LHD: $rule"
111+
)
112+
}
113+
case None => // ignore
114+
}
115+
}
116+
}
117+
}
118+
119+
new Rul2Model(lookupRuleRhd, lookupRuleLhd)
120+
}
121+
122+
def evaluateRulesOnce(lookupRule: PartialFunction[EquivRule, Rule[IdTile]], t0: IdTile, t1: IdTile): Option[(IdTile, IdTile)] = {
123+
val key = new EquivRule(Rule(t0, t1, t0, t1))
124+
lookupRule.unapply(key).flatMap(rule => applyRule(rule, t0, t1))
125+
}
126+
127+
}
128+
129+
/** Holds all RUL2 code in memory. Instantiate this with `Rul2Model.load`. */
130+
class Rul2Model private (
131+
val lookupRuleRhd: PartialFunction[EquivRule, Rule[IdTile]],
132+
val lookupRuleLhd: PartialFunction[EquivRule, Rule[IdTile]],
133+
)

src/main/scala/module/SanityChecker.scala

Lines changed: 5 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,12 @@ import java.nio.file.{Files, Paths, Path}
44
import resource._
55
import io.github.memo33.metarules.meta.{RotFlip, Rule, EquivRule, IdTile}
66
import syntax.IdTile
7+
import Rul2Model.{iterateRulFiles, parseRule}
78

89
/** Run with `sbt "runMain com.sc4nam.module.SanityChecker"`.
910
*/
1011
object SanityChecker {
1112

12-
sealed trait Driveside
13-
case object Rhd extends Driveside
14-
case object Lhd extends Driveside
15-
case object RhdAndLhd extends Driveside
16-
1713
val linePatternIncludingNewlines = "(?<=\n|\r(?!\n))"
1814

1915
def main(args: Array[String]): Unit = {
@@ -36,8 +32,8 @@ object SanityChecker {
3632

3733
// first cache all the RUL2 code generated by metarules
3834
LOGGER.info("caching RUL2 code generated by metarules")
39-
Files.list(Paths.get("target")).forEach { path =>
40-
if (isRulFile(path) && isMetaruleFile(path)) {
35+
iterateRulFiles(Paths.get("target")).foreach { path =>
36+
if (isMetaruleFile(path)) {
4137
for (scanner <- managed(new java.util.Scanner(path.toFile(), "UTF-8"))) {
4238
while(scanner.hasNextLine()) {
4339
parseRule(scanner.nextLine()).foreach { rule =>
@@ -50,8 +46,8 @@ object SanityChecker {
5046

5147
// then check handwritten RUL2 code for redundancies
5248
LOGGER.info("searching for redundant handwritten RUL2 code")
53-
Files.walk(Paths.get("Controller/RUL2")).forEach { path =>
54-
if (isRulFile(path) && !isMetaruleFile(path)) {
49+
iterateRulFiles(Paths.get("Controller/RUL2")).foreach { path =>
50+
if (!isMetaruleFile(path)) {
5551
val tmpPath = path.resolveSibling(path.getFileName().toString() + ".tmp")
5652
val endsWithNewline = fileEndsWithNewline(path) // attempt to preserve missing newlines at end of files to avoid noise
5753
scala.util.Using.resources(
@@ -80,26 +76,6 @@ object SanityChecker {
8076
metaruleFiles.contains(path.getFileName().toString())
8177
}
8278

83-
def isRulFile(path: Path) = {
84-
val s = path.getFileName().toString()
85-
s.endsWith(".txt") || s.endsWith(".rul")
86-
}
87-
88-
def parseRule(line: String): Option[Rule[IdTile]] = {
89-
val chunk = line.split(";|\\[", 2)(0).trim
90-
if (chunk.isEmpty) {
91-
None
92-
} else try {
93-
val ts = chunk.split(",|=").grouped(3).toSeq.map(tup =>
94-
IdTile(java.lang.Long.decode(tup(0)).toInt, RotFlip(tup(1).toInt, tup(2).toInt)))
95-
Some(Rule(ts(0), ts(1), ts(2), ts(3)))
96-
} catch {
97-
case _: IllegalArgumentException => // syntax errors in RUL2 code
98-
// throw new IllegalArgumentException(line)
99-
None
100-
}
101-
}
102-
10379
def fileEndsWithNewline(path: Path): Boolean = {
10480
managed(new java.io.RandomAccessFile(path.toFile(), "r")) acquireAndGet { raf =>
10581
val size = Files.size(path)

src/main/scala/scripts/RedundantAdjacenciesChecker.scala

Lines changed: 19 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import com.sc4nam.module._
55
import io.github.memo33.metarules.meta.{RotFlip, Rule, EquivRule, IdTile}
66
import RotFlip._
77
import syntax.IdTile
8-
import SanityChecker.{isRulFile, fileEndsWithNewline, Driveside, Rhd, Lhd, RhdAndLhd, parseRule, linePatternIncludingNewlines}
8+
import SanityChecker.{fileEndsWithNewline, linePatternIncludingNewlines}
9+
import Rul2Model.{iterateRulFiles, parseRuleWithRestrictedDriveside, Rhd, Lhd, RhdAndLhd, drivesideOfFile, applyRule}
910

1011
/** Run with `SBT_OPTS="-Xmx2G" sbt "runMain com.sc4nam.scripts.RedundantAdjacenciesChecker"`.
1112
* Note that this increases the heap size for more memory.
@@ -25,57 +26,13 @@ object RedundantAdjacenciesChecker {
2526
* redundant code.
2627
*/
2728
def main(args: Array[String]): Unit = {
28-
checkRedundantAdjacencies()
29+
val rul2 = Rul2Model.load(Paths.get("Controller/RUL2"))
30+
checkRedundantAdjacencies(rul2)
2931
}
3032

31-
def drivesideOfFile(path: Path): Driveside = {
32-
val name = path.getFileName().toString()
33-
if (name.contains("rhd.")) Rhd
34-
else if (name.contains("lhd.")) Lhd
35-
else RhdAndLhd
36-
}
37-
38-
val lhdPrefix = ";###LHD###"
39-
val rhdPrefix = ";###RHD###"
40-
41-
def parseRuleWithRestrictedDriveside(line: String, driveside: Driveside): Option[(Rule[IdTile], Driveside)] = {
42-
val line1 = line.trim()
43-
if (line1.startsWith(lhdPrefix)) {
44-
if (driveside == Rhd) None else parseRule(line1.substring(lhdPrefix.length)).map(_ -> Lhd)
45-
} else if (line1.startsWith(rhdPrefix)) {
46-
if (driveside == Lhd) None else parseRule(line1.substring(rhdPrefix.length)).map(_ -> Rhd)
47-
} else {
48-
parseRule(line1).map(_ -> driveside)
49-
}
50-
}
51-
52-
def checkRedundantAdjacencies(): Unit = {
53-
val rulesRhd = collection.mutable.Map.empty[EquivRule, Rule[IdTile]]
54-
val rulesLhd = collection.mutable.Map.empty[EquivRule, Rule[IdTile]]
55-
val rulesShared = collection.mutable.Map.empty[EquivRule, Rule[IdTile]]
56-
val lookupRuleRhd: PartialFunction[EquivRule, Rule[IdTile]] = rulesShared.orElse(rulesRhd) // the two maps should be disjoint
57-
val lookupRuleLhd: PartialFunction[EquivRule, Rule[IdTile]] = rulesShared.orElse(rulesRhd) // the two maps should be disjoint
58-
59-
LOGGER.info("caching all RUL2 code for RHD and LHD")
60-
Files.walk(Paths.get("Controller/RUL2")).forEach { path =>
61-
if (isRulFile(path)) {
62-
val drivesideFile = drivesideOfFile(path)
63-
scala.util.Using.resource(new java.util.Scanner(path.toFile(), "UTF-8")) { scanner =>
64-
while(scanner.hasNextLine()) {
65-
parseRuleWithRestrictedDriveside(scanner.nextLine(), drivesideFile) match {
66-
case Some((rule, Rhd)) => rulesRhd.addOne(new EquivRule(rule), rule)
67-
case Some((rule, Lhd)) => rulesLhd.addOne(new EquivRule(rule), rule)
68-
case Some((rule, RhdAndLhd)) => rulesShared.addOne(new EquivRule(rule), rule)
69-
case None => // ignore
70-
}
71-
}
72-
}
73-
}
74-
}
75-
76-
LOGGER.info("searching for redundant adjacencies in RUL2 code")
77-
Files.walk(Paths.get("Controller/RUL2")).forEach { path =>
78-
if (isRulFile(path)) {
33+
def checkRedundantAdjacencies(rul2: Rul2Model): Unit = {
34+
LOGGER.info("Searching for redundant adjacencies in RUL2 code")
35+
iterateRulFiles(Paths.get("Controller/RUL2")).foreach { path =>
7936
val drivesideFile = drivesideOfFile(path)
8037
val tmpPath = path.resolveSibling(path.getFileName().toString() + ".tmp")
8138
val endsWithNewline = fileEndsWithNewline(path) // attempt to preserve missing newlines at end of files to avoid noise
@@ -88,13 +45,14 @@ object RedundantAdjacenciesChecker {
8845

8946
val redundant =
9047
parseRuleWithRestrictedDriveside(line, drivesideFile) match {
91-
case Some((rule, Rhd)) => isRedundantAdjacency(rule, lookupRuleRhd)
92-
case Some((rule, Lhd)) => isRedundantAdjacency(rule, lookupRuleLhd)
48+
case Some((rule, Rhd)) => isRedundantAdjacency(rule, rul2.lookupRuleRhd)
49+
case Some((rule, Lhd)) => isRedundantAdjacency(rule, rul2.lookupRuleLhd)
9350
case Some((rule, RhdAndLhd)) =>
94-
val b = isRedundantAdjacency(rule, lookupRuleRhd)
95-
if (b != isRedundantAdjacency(rule, lookupRuleLhd)) {
96-
throw new AssertionError("Redundancies should be the same for RHD and LHD.") // hopefully this will always be the case
97-
}
51+
val b = isRedundantAdjacency(rule, rul2.lookupRuleRhd)
52+
require(
53+
b == isRedundantAdjacency(rule, rul2.lookupRuleLhd),
54+
s"Redundancies should be the same for RHD and LHD: $rule" // hopefully this will always be the case
55+
)
9856
b
9957
case None => false // comments are not redundant
10058
}
@@ -107,7 +65,6 @@ object RedundantAdjacenciesChecker {
10765
}
10866
}
10967
Files.move(tmpPath, path, java.nio.file.StandardCopyOption.REPLACE_EXISTING)
110-
}
11168
}
11269
}
11370

@@ -139,27 +96,14 @@ object RedundantAdjacenciesChecker {
13996
(a * rot, b * rot, southBound)
14097
}}
14198

142-
def evaluateRule(rule: Rule[IdTile], t0: IdTile, t1: IdTile): Option[(IdTile, IdTile)] = {
143-
if (t0 == rule(0) && t1 == rule(1)) Some((rule(2), rule(3)))
144-
else if (t0 == rule(0) * R2F1 && t1 == rule(1) * R2F1) Some((rule(2) * R2F1, rule(3) * R2F1))
145-
else if (t0 == rule(1) * R0F1 && t1 == rule(0) * R0F1) Some((rule(3) * R0F1, rule(2) * R0F1))
146-
else if (t0 == rule(1) * R2F0 && t1 == rule(0) * R2F0) Some((rule(3) * R2F0, rule(2) * R2F0))
147-
else None
148-
}
149-
150-
def evaluateRulesOnce(lookupRule: PartialFunction[EquivRule, Rule[IdTile]], t0: IdTile, t1: IdTile): Option[(IdTile, IdTile)] = {
151-
val key = new EquivRule(Rule(t0, t1, t0, t1))
152-
lookupRule.unapply(key).flatMap(rule => evaluateRule(rule, t0, t1))
153-
}
154-
15599
/** Checks if adjacency overrides a -> b and b -> c exist (in that order and
156100
* direction) (where b is surrogate tile) and matches expected result
157101
* aExpected, cExpected.
158102
*/
159103
def connectingOrthOverridesExist(lookupRule: PartialFunction[EquivRule, Rule[IdTile]], a: IdTile, b: IdTile, c: IdTile, aExpected: IdTile, cExpected: IdTile): Boolean = {
160-
evaluateRulesOnce(lookupRule, a, b) match {
104+
Rul2Model.evaluateRulesOnce(lookupRule, a, b) match {
161105
case Some((a1, b1)) if a1 == a && a1.id != b1.id =>
162-
evaluateRulesOnce(lookupRule, b1, c) match {
106+
Rul2Model.evaluateRulesOnce(lookupRule, b1, c) match {
163107
case Some((b2, c2)) if b2 == b1 && b2.id != c2.id && c2 != c =>
164108
// found two overrides connecting a to c, so check if result of their application is as expected
165109
a1 == aExpected && c2 == cExpected
@@ -175,15 +119,15 @@ object RedundantAdjacenciesChecker {
175119
* northbound direction.
176120
*/
177121
def connectingDiagOverridesExist(lookupRule: PartialFunction[EquivRule, Rule[IdTile]], a: IdTile, b: IdTile, c: IdTile, d: IdTile, southBound: Boolean, aExpected: IdTile, dExpected: IdTile): Boolean = {
178-
evaluateRulesOnce(lookupRule, a, b) match {
122+
Rul2Model.evaluateRulesOnce(lookupRule, a, b) match {
179123
case Some((a1, b1)) if a1 == a && a1.id != b1.id =>
180124
val rot = if (southBound) R3F0 else R1F0
181-
evaluateRulesOnce(lookupRule, b1 * rot, c * rot) match {
125+
Rul2Model.evaluateRulesOnce(lookupRule, b1 * rot, c * rot) match {
182126
case Some((b2rot, c2rot)) =>
183127
val b2 = b2rot * (R0F0 / rot)
184128
val c2 = c2rot * (R0F0 / rot)
185129
if (b2 == b1 && c2 != c) {
186-
evaluateRulesOnce(lookupRule, c2, d) match {
130+
Rul2Model.evaluateRulesOnce(lookupRule, c2, d) match {
187131
case Some((c3, d3)) if c3 == c2 && d3.id != c3.id && d3.id != b2.id && d3 != d =>
188132
a1 == aExpected && d3 == dExpected
189133
case _ => false

0 commit comments

Comments
 (0)