Skip to content

Commit 18ab03b

Browse files
committed
refactoring: move common RUL2-related functions to new file, ensure deterministic load order
This mainly introduces a new function `Rul2Model.iterateRulFiles` which deterministicly walks a RUL file tree in the same order that is used by the NAMControllerCompiler.
1 parent 1a05326 commit 18ab03b

3 files changed

Lines changed: 90 additions & 66 deletions

File tree

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package com.sc4nam.module
2+
3+
import java.nio.file.{Files, Paths, Path}
4+
import io.github.memo33.metarules.meta.{RotFlip, 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+
}

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: 5 additions & 37 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.
@@ -28,27 +29,6 @@ object RedundantAdjacenciesChecker {
2829
checkRedundantAdjacencies()
2930
}
3031

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-
5232
def checkRedundantAdjacencies(): Unit = {
5333
val rulesRhd = collection.mutable.Map.empty[EquivRule, Rule[IdTile]]
5434
val rulesLhd = collection.mutable.Map.empty[EquivRule, Rule[IdTile]]
@@ -57,8 +37,7 @@ object RedundantAdjacenciesChecker {
5737
val lookupRuleLhd: PartialFunction[EquivRule, Rule[IdTile]] = rulesShared.orElse(rulesLhd) // the two maps should be disjoint
5838

5939
LOGGER.info("caching all RUL2 code for RHD and LHD")
60-
Files.walk(Paths.get("Controller/RUL2")).forEach { path =>
61-
if (isRulFile(path)) {
40+
iterateRulFiles(Paths.get("Controller/RUL2")).foreach { path =>
6241
val drivesideFile = drivesideOfFile(path)
6342
scala.util.Using.resource(new java.util.Scanner(path.toFile(), "UTF-8")) { scanner =>
6443
while(scanner.hasNextLine()) {
@@ -70,12 +49,10 @@ object RedundantAdjacenciesChecker {
7049
}
7150
}
7251
}
73-
}
7452
}
7553

7654
LOGGER.info("searching for redundant adjacencies in RUL2 code")
77-
Files.walk(Paths.get("Controller/RUL2")).forEach { path =>
78-
if (isRulFile(path)) {
55+
iterateRulFiles(Paths.get("Controller/RUL2")).foreach { path =>
7956
val drivesideFile = drivesideOfFile(path)
8057
val tmpPath = path.resolveSibling(path.getFileName().toString() + ".tmp")
8158
val endsWithNewline = fileEndsWithNewline(path) // attempt to preserve missing newlines at end of files to avoid noise
@@ -107,7 +84,6 @@ object RedundantAdjacenciesChecker {
10784
}
10885
}
10986
Files.move(tmpPath, path, java.nio.file.StandardCopyOption.REPLACE_EXISTING)
110-
}
11187
}
11288
}
11389

@@ -139,17 +115,9 @@ object RedundantAdjacenciesChecker {
139115
(a * rot, b * rot, southBound)
140116
}}
141117

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-
150118
def evaluateRulesOnce(lookupRule: PartialFunction[EquivRule, Rule[IdTile]], t0: IdTile, t1: IdTile): Option[(IdTile, IdTile)] = {
151119
val key = new EquivRule(Rule(t0, t1, t0, t1))
152-
lookupRule.unapply(key).flatMap(rule => evaluateRule(rule, t0, t1))
120+
lookupRule.unapply(key).flatMap(rule => applyRule(rule, t0, t1))
153121
}
154122

155123
/** Checks if adjacency overrides a -> b and b -> c exist (in that order and

0 commit comments

Comments
 (0)