First INIT
This commit is contained in:
27
.classpath
Normal file
27
.classpath
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<classpath>
|
||||||
|
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-17">
|
||||||
|
<attributes>
|
||||||
|
<attribute name="maven.pomderived" value="true"/>
|
||||||
|
</attributes>
|
||||||
|
</classpathentry>
|
||||||
|
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
|
||||||
|
<attributes>
|
||||||
|
<attribute name="maven.pomderived" value="true"/>
|
||||||
|
</attributes>
|
||||||
|
</classpathentry>
|
||||||
|
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
|
||||||
|
<attributes>
|
||||||
|
<attribute name="maven.pomderived" value="true"/>
|
||||||
|
<attribute name="optional" value="true"/>
|
||||||
|
</attributes>
|
||||||
|
</classpathentry>
|
||||||
|
<classpathentry excluding="**" kind="src" output="target/test-classes" path="src/test/resources">
|
||||||
|
<attributes>
|
||||||
|
<attribute name="maven.pomderived" value="true"/>
|
||||||
|
<attribute name="test" value="true"/>
|
||||||
|
<attribute name="optional" value="true"/>
|
||||||
|
</attributes>
|
||||||
|
</classpathentry>
|
||||||
|
<classpathentry kind="output" path="target/classes"/>
|
||||||
|
</classpath>
|
||||||
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/bin/
|
||||||
|
/target/
|
||||||
23
.project
Normal file
23
.project
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<projectDescription>
|
||||||
|
<name>DQFL-Library</name>
|
||||||
|
<comment></comment>
|
||||||
|
<projects>
|
||||||
|
</projects>
|
||||||
|
<buildSpec>
|
||||||
|
<buildCommand>
|
||||||
|
<name>org.eclipse.jdt.core.javabuilder</name>
|
||||||
|
<arguments>
|
||||||
|
</arguments>
|
||||||
|
</buildCommand>
|
||||||
|
<buildCommand>
|
||||||
|
<name>org.eclipse.m2e.core.maven2Builder</name>
|
||||||
|
<arguments>
|
||||||
|
</arguments>
|
||||||
|
</buildCommand>
|
||||||
|
</buildSpec>
|
||||||
|
<natures>
|
||||||
|
<nature>org.eclipse.m2e.core.maven2Nature</nature>
|
||||||
|
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||||
|
</natures>
|
||||||
|
</projectDescription>
|
||||||
2
.settings/org.eclipse.core.resources.prefs
Normal file
2
.settings/org.eclipse.core.resources.prefs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
eclipse.preferences.version=1
|
||||||
|
encoding/<project>=UTF-8
|
||||||
15
.settings/org.eclipse.jdt.core.prefs
Normal file
15
.settings/org.eclipse.jdt.core.prefs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
eclipse.preferences.version=1
|
||||||
|
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
|
||||||
|
org.eclipse.jdt.core.compiler.codegen.targetPlatform=17
|
||||||
|
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
|
||||||
|
org.eclipse.jdt.core.compiler.compliance=17
|
||||||
|
org.eclipse.jdt.core.compiler.debug.lineNumber=generate
|
||||||
|
org.eclipse.jdt.core.compiler.debug.localVariable=generate
|
||||||
|
org.eclipse.jdt.core.compiler.debug.sourceFile=generate
|
||||||
|
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
|
||||||
|
org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
|
||||||
|
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
|
||||||
|
org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
|
||||||
|
org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning
|
||||||
|
org.eclipse.jdt.core.compiler.release=enabled
|
||||||
|
org.eclipse.jdt.core.compiler.source=17
|
||||||
4
.settings/org.eclipse.m2e.core.prefs
Normal file
4
.settings/org.eclipse.m2e.core.prefs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
activeProfiles=
|
||||||
|
eclipse.preferences.version=1
|
||||||
|
resolveWorkspaceProjects=true
|
||||||
|
version=1
|
||||||
54
pom.xml
Normal file
54
pom.xml
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<groupId>de.dogfire.dqfl</groupId>
|
||||||
|
<artifactId>dqfl-core</artifactId>
|
||||||
|
<version>1.0.0-SNAPSHOT</version>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
|
<name>DQFL Core Library</name>
|
||||||
|
<description>Dogfire Quiz Flow Language V1 - Parser, Validator, and Model</description>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
<maven.compiler.release>17</maven.compiler.release>
|
||||||
|
<maven.compiler.source>17</maven.compiler.source>
|
||||||
|
<maven.compiler.target>17</maven.compiler.target>
|
||||||
|
<java.version>17</java.version>
|
||||||
|
<junit.version>5.10.2</junit.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter</artifactId>
|
||||||
|
<version>${junit.version}</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.11.0</version>
|
||||||
|
<configuration>
|
||||||
|
<release>${maven.compiler.release}</release>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-surefire-plugin</artifactId>
|
||||||
|
<version>3.2.5</version>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-jar-plugin</artifactId>
|
||||||
|
<version>3.3.0</version>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
117
src/main/java/de/dogfire/dqfl/Dqfl.java
Normal file
117
src/main/java/de/dogfire/dqfl/Dqfl.java
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
|
||||||
|
package de.dogfire.dqfl;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import de.dogfire.dqfl.error.DqflParseException;
|
||||||
|
import de.dogfire.dqfl.error.DqflValidationResult;
|
||||||
|
import de.dogfire.dqfl.model.DqflDocument;
|
||||||
|
import de.dogfire.dqfl.parser.DqflParser;
|
||||||
|
import de.dogfire.dqfl.validator.DqflValidator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zentrale Fassade der DQFL V1 Library.
|
||||||
|
* Bietet bequeme Methoden zum Laden, Parsen und Validieren von DQFL-Dokumenten.
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* // Beispiel: Datei laden, parsen und validieren
|
||||||
|
* DqflDocument doc = Dqfl.parseFile(path);
|
||||||
|
* DqflValidationResult result = Dqfl.validate(doc);
|
||||||
|
* if (!result.isValid()) { ... }
|
||||||
|
*
|
||||||
|
* // Beispiel: String direkt parsen
|
||||||
|
* DqflDocument doc = Dqfl.parse(dqflString);
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
public final class Dqfl
|
||||||
|
{
|
||||||
|
private Dqfl()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parst einen DQFL-String und gibt das Dokumentmodell zurück.
|
||||||
|
*
|
||||||
|
* @throws DqflParseException bei Syntaxfehlern
|
||||||
|
*/
|
||||||
|
public static DqflDocument parse(final String input) throws DqflParseException
|
||||||
|
{
|
||||||
|
return new DqflParser().parse(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liest und parst eine .dqfl-Datei.
|
||||||
|
*
|
||||||
|
* @throws DqflParseException bei Syntaxfehlern
|
||||||
|
* @throws IOException bei Dateizugriffsfehlern
|
||||||
|
*/
|
||||||
|
public static DqflDocument parseFile(final Path path) throws DqflParseException, IOException
|
||||||
|
{
|
||||||
|
final String content = Files.readString(path, StandardCharsets.UTF_8);
|
||||||
|
return Dqfl.parse(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liest und parst aus einem InputStream (z.B. Classpath-Resource).
|
||||||
|
*
|
||||||
|
* @throws DqflParseException bei Syntaxfehlern
|
||||||
|
* @throws IOException bei Lesefehlern
|
||||||
|
*/
|
||||||
|
public static DqflDocument parseStream(final InputStream stream) throws DqflParseException, IOException
|
||||||
|
{
|
||||||
|
final String content = new String(stream.readAllBytes(), StandardCharsets.UTF_8);
|
||||||
|
return Dqfl.parse(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validiert ein geparstes DqflDocument gegen alle V1-Fachregeln.
|
||||||
|
*/
|
||||||
|
public static DqflValidationResult validate(final DqflDocument document)
|
||||||
|
{
|
||||||
|
return new DqflValidator().validate(document);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parst und validiert in einem Schritt.
|
||||||
|
* Gibt das Dokument nur zurück wenn die Validierung keine Fehler enthält.
|
||||||
|
*
|
||||||
|
* @throws DqflParseException bei Syntaxfehlern
|
||||||
|
* @throws DqflValidationException wenn die Validierung fehlschlägt
|
||||||
|
*/
|
||||||
|
public static DqflDocument parseAndValidate(final String input)
|
||||||
|
throws DqflParseException, DqflValidationException
|
||||||
|
{
|
||||||
|
final DqflDocument doc = Dqfl.parse(input);
|
||||||
|
final DqflValidationResult result = Dqfl.validate(doc);
|
||||||
|
|
||||||
|
if(!result.isValid())
|
||||||
|
{
|
||||||
|
throw new DqflValidationException(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception die geworfen wird wenn ein Dokument die Validierung nicht besteht.
|
||||||
|
*/
|
||||||
|
public static class DqflValidationException extends Exception
|
||||||
|
{
|
||||||
|
private final DqflValidationResult result;
|
||||||
|
|
||||||
|
public DqflValidationException(final DqflValidationResult result)
|
||||||
|
{
|
||||||
|
super("DQFL validation failed: " + result.getErrorsOnly().size() + " error(s)");
|
||||||
|
this.result = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DqflValidationResult getResult()
|
||||||
|
{
|
||||||
|
return this.result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
79
src/main/java/de/dogfire/dqfl/error/DqflError.java
Normal file
79
src/main/java/de/dogfire/dqfl/error/DqflError.java
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
|
||||||
|
package de.dogfire.dqfl.error;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Einzelner Fehler aus Parsing oder Validierung.
|
||||||
|
*/
|
||||||
|
public final class DqflError
|
||||||
|
{
|
||||||
|
public enum Severity
|
||||||
|
{
|
||||||
|
ERROR,
|
||||||
|
WARNING
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Severity severity;
|
||||||
|
private final int line;
|
||||||
|
private final String message;
|
||||||
|
|
||||||
|
public DqflError(final Severity severity, final int line, final String message)
|
||||||
|
{
|
||||||
|
this.severity = Objects.requireNonNull(severity);
|
||||||
|
this.line = line;
|
||||||
|
this.message = Objects.requireNonNull(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DqflError error(final int line, final String message)
|
||||||
|
{
|
||||||
|
return new DqflError(Severity.ERROR, line, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DqflError warning(final int line, final String message)
|
||||||
|
{
|
||||||
|
return new DqflError(Severity.WARNING, line, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Fehler ohne Zeilenbezug (z.B. Validierung auf Dokumentebene). */
|
||||||
|
public static DqflError error(final String message)
|
||||||
|
{
|
||||||
|
return new DqflError(Severity.ERROR, -1, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DqflError warning(final String message)
|
||||||
|
{
|
||||||
|
return new DqflError(Severity.WARNING, -1, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Severity getSeverity()
|
||||||
|
{
|
||||||
|
return this.severity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getLine()
|
||||||
|
{
|
||||||
|
return this.line;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMessage()
|
||||||
|
{
|
||||||
|
return this.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isError()
|
||||||
|
{
|
||||||
|
return this.severity == Severity.ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
final String prefix = this.severity == Severity.ERROR ? "ERROR" : "WARN";
|
||||||
|
if(this.line > 0)
|
||||||
|
{
|
||||||
|
return "[" + prefix + " line " + this.line + "] " + this.message;
|
||||||
|
}
|
||||||
|
return "[" + prefix + "] " + this.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
42
src/main/java/de/dogfire/dqfl/error/DqflParseException.java
Normal file
42
src/main/java/de/dogfire/dqfl/error/DqflParseException.java
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
|
||||||
|
package de.dogfire.dqfl.error;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception die geworfen wird wenn das Parsen eines DQFL-Dokuments fehlschlägt.
|
||||||
|
* Enthält alle gesammelten Fehler.
|
||||||
|
*/
|
||||||
|
public class DqflParseException extends Exception
|
||||||
|
{
|
||||||
|
private final List<DqflError> errors;
|
||||||
|
|
||||||
|
public DqflParseException(final String message, final List<DqflError> errors)
|
||||||
|
{
|
||||||
|
super(message);
|
||||||
|
this.errors = Collections.unmodifiableList(errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DqflParseException(final String message)
|
||||||
|
{
|
||||||
|
super(message);
|
||||||
|
this.errors = List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<DqflError> getErrors()
|
||||||
|
{
|
||||||
|
return this.errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
final StringBuilder sb = new StringBuilder(this.getMessage());
|
||||||
|
for(final DqflError error : this.errors)
|
||||||
|
{
|
||||||
|
sb.append("\n ").append(error);
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
|
||||||
|
package de.dogfire.dqfl.error;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ergebnis einer DQFL-Validierung.
|
||||||
|
*/
|
||||||
|
public final class DqflValidationResult
|
||||||
|
{
|
||||||
|
private final List<DqflError> errors;
|
||||||
|
|
||||||
|
public DqflValidationResult(final List<DqflError> errors)
|
||||||
|
{
|
||||||
|
this.errors = Collections.unmodifiableList(new ArrayList<>(errors));
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isValid()
|
||||||
|
{
|
||||||
|
return this.errors.stream().noneMatch(DqflError::isError);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasWarnings()
|
||||||
|
{
|
||||||
|
return this.errors.stream().anyMatch(e -> !e.isError());
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<DqflError> getErrors()
|
||||||
|
{
|
||||||
|
return this.errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<DqflError> getErrorsOnly()
|
||||||
|
{
|
||||||
|
return this.errors.stream().filter(DqflError::isError).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<DqflError> getWarningsOnly()
|
||||||
|
{
|
||||||
|
return this.errors.stream().filter(e -> !e.isError()).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
if(this.isValid() && !this.hasWarnings())
|
||||||
|
{
|
||||||
|
return "Validation OK";
|
||||||
|
}
|
||||||
|
final StringBuilder sb = new StringBuilder("Validation ");
|
||||||
|
sb.append(this.isValid() ? "OK (with warnings)" : "FAILED");
|
||||||
|
for(final DqflError error : this.errors)
|
||||||
|
{
|
||||||
|
sb.append("\n ").append(error);
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
130
src/main/java/de/dogfire/dqfl/model/DqflCategory.java
Normal file
130
src/main/java/de/dogfire/dqfl/model/DqflCategory.java
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
|
||||||
|
package de.dogfire.dqfl.model;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repräsentiert einen CATEGORY-Block innerhalb einer ROUND.
|
||||||
|
*/
|
||||||
|
public final class DqflCategory
|
||||||
|
{
|
||||||
|
private final String alias;
|
||||||
|
private final String title;
|
||||||
|
private final String usePool;
|
||||||
|
private final LayoutType layout;
|
||||||
|
private final GridSize size;
|
||||||
|
private final List<DqflPhase> phases;
|
||||||
|
|
||||||
|
private DqflCategory(final Builder builder)
|
||||||
|
{
|
||||||
|
this.alias = Objects.requireNonNull(builder.alias, "alias must not be null");
|
||||||
|
this.title = builder.title;
|
||||||
|
this.usePool = builder.usePool;
|
||||||
|
this.layout = builder.layout;
|
||||||
|
this.size = builder.size;
|
||||||
|
this.phases = Collections.unmodifiableList(new ArrayList<>(builder.phases));
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAlias()
|
||||||
|
{
|
||||||
|
return this.alias;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Kann null sein — TITLE ist optional. */
|
||||||
|
public String getTitle()
|
||||||
|
{
|
||||||
|
return this.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUsePool()
|
||||||
|
{
|
||||||
|
return this.usePool;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LayoutType getLayout()
|
||||||
|
{
|
||||||
|
return this.layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Kann null sein — nur gültig zusammen mit LAYOUT. */
|
||||||
|
public GridSize getSize()
|
||||||
|
{
|
||||||
|
return this.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<DqflPhase> getPhases()
|
||||||
|
{
|
||||||
|
return this.phases;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
return "CATEGORY " + this.alias
|
||||||
|
+ " [title=" + this.title
|
||||||
|
+ ", pool=" + this.usePool
|
||||||
|
+ ", phases=" + this.phases.size() + "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// Builder
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
public static Builder builder(final String alias)
|
||||||
|
{
|
||||||
|
return new Builder(alias);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class Builder
|
||||||
|
{
|
||||||
|
private final String alias;
|
||||||
|
private String title;
|
||||||
|
private String usePool;
|
||||||
|
private LayoutType layout;
|
||||||
|
private GridSize size;
|
||||||
|
private final List<DqflPhase> phases = new ArrayList<>();
|
||||||
|
|
||||||
|
private Builder(final String alias)
|
||||||
|
{
|
||||||
|
this.alias = alias;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder title(final String title)
|
||||||
|
{
|
||||||
|
this.title = title;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder usePool(final String usePool)
|
||||||
|
{
|
||||||
|
this.usePool = usePool;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder layout(final LayoutType layout)
|
||||||
|
{
|
||||||
|
this.layout = layout;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder size(final GridSize size)
|
||||||
|
{
|
||||||
|
this.size = size;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder addPhase(final DqflPhase phase)
|
||||||
|
{
|
||||||
|
this.phases.add(Objects.requireNonNull(phase));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DqflCategory build()
|
||||||
|
{
|
||||||
|
return new DqflCategory(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
162
src/main/java/de/dogfire/dqfl/model/DqflDocument.java
Normal file
162
src/main/java/de/dogfire/dqfl/model/DqflDocument.java
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
|
||||||
|
package de.dogfire.dqfl.model;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repräsentiert ein vollständig geparstes DQFL-Dokument.
|
||||||
|
* Dies ist der Root-Knoten des AST.
|
||||||
|
*/
|
||||||
|
public final class DqflDocument
|
||||||
|
{
|
||||||
|
private final String dqflVersion;
|
||||||
|
private final String flowName;
|
||||||
|
private final Integer scriptRevision;
|
||||||
|
private final Map<String, DqflSource> sources;
|
||||||
|
private final Map<String, DqflPool> pools;
|
||||||
|
private final List<DqflRound> rounds;
|
||||||
|
|
||||||
|
private DqflDocument(final Builder builder)
|
||||||
|
{
|
||||||
|
this.dqflVersion = Objects.requireNonNull(builder.dqflVersion, "dqflVersion must not be null");
|
||||||
|
this.flowName = Objects.requireNonNull(builder.flowName, "flowName must not be null");
|
||||||
|
this.scriptRevision = builder.scriptRevision;
|
||||||
|
this.sources = Collections.unmodifiableMap(new LinkedHashMap<>(builder.sources));
|
||||||
|
this.pools = Collections.unmodifiableMap(new LinkedHashMap<>(builder.pools));
|
||||||
|
this.rounds = Collections.unmodifiableList(new ArrayList<>(builder.rounds));
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDqflVersion()
|
||||||
|
{
|
||||||
|
return this.dqflVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFlowName()
|
||||||
|
{
|
||||||
|
return this.flowName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Kann null sein — SCRIPT_REVISION ist optional. */
|
||||||
|
public Integer getScriptRevision()
|
||||||
|
{
|
||||||
|
return this.scriptRevision;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, DqflSource> getSources()
|
||||||
|
{
|
||||||
|
return this.sources;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DqflSource getSource(final String alias)
|
||||||
|
{
|
||||||
|
return this.sources.get(alias);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, DqflPool> getPools()
|
||||||
|
{
|
||||||
|
return this.pools;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DqflPool getPool(final String alias)
|
||||||
|
{
|
||||||
|
return this.pools.get(alias);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<DqflRound> getRounds()
|
||||||
|
{
|
||||||
|
return this.rounds;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt eine flache Liste aller Kategorien über alle Runden zurück,
|
||||||
|
* in Dokumentreihenfolge.
|
||||||
|
*/
|
||||||
|
public List<DqflCategory> getAllCategoriesFlat()
|
||||||
|
{
|
||||||
|
final List<DqflCategory> all = new ArrayList<>();
|
||||||
|
for(final DqflRound round : this.rounds)
|
||||||
|
{
|
||||||
|
all.addAll(round.getCategories());
|
||||||
|
}
|
||||||
|
return Collections.unmodifiableList(all);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
return "DqflDocument [version=" + this.dqflVersion
|
||||||
|
+ ", flow=" + this.flowName
|
||||||
|
+ ", sources=" + this.sources.size()
|
||||||
|
+ ", pools=" + this.pools.size()
|
||||||
|
+ ", rounds=" + this.rounds.size() + "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// Builder
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
public static Builder builder()
|
||||||
|
{
|
||||||
|
return new Builder();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class Builder
|
||||||
|
{
|
||||||
|
private String dqflVersion;
|
||||||
|
private String flowName;
|
||||||
|
private Integer scriptRevision;
|
||||||
|
private final Map<String, DqflSource> sources = new LinkedHashMap<>();
|
||||||
|
private final Map<String, DqflPool> pools = new LinkedHashMap<>();
|
||||||
|
private final List<DqflRound> rounds = new ArrayList<>();
|
||||||
|
|
||||||
|
private Builder()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder dqflVersion(final String dqflVersion)
|
||||||
|
{
|
||||||
|
this.dqflVersion = dqflVersion;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder flowName(final String flowName)
|
||||||
|
{
|
||||||
|
this.flowName = flowName;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder scriptRevision(final Integer scriptRevision)
|
||||||
|
{
|
||||||
|
this.scriptRevision = scriptRevision;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder addSource(final DqflSource source)
|
||||||
|
{
|
||||||
|
this.sources.put(source.getAlias(), source);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder addPool(final DqflPool pool)
|
||||||
|
{
|
||||||
|
this.pools.put(pool.getAlias(), pool);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder addRound(final DqflRound round)
|
||||||
|
{
|
||||||
|
this.rounds.add(Objects.requireNonNull(round));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DqflDocument build()
|
||||||
|
{
|
||||||
|
return new DqflDocument(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
167
src/main/java/de/dogfire/dqfl/model/DqflPhase.java
Normal file
167
src/main/java/de/dogfire/dqfl/model/DqflPhase.java
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
|
||||||
|
package de.dogfire.dqfl.model;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repräsentiert einen PHASE-Block innerhalb einer CATEGORY.
|
||||||
|
*/
|
||||||
|
public final class DqflPhase
|
||||||
|
{
|
||||||
|
private final PhaseType phaseType;
|
||||||
|
private final QuestionSetType questionSet;
|
||||||
|
private final Integer cycles;
|
||||||
|
private final Integer turnsPerCycle;
|
||||||
|
private final TurnOrderType turnOrder;
|
||||||
|
private final PickModeType pickMode;
|
||||||
|
private final TeamModeType teamMode;
|
||||||
|
private final TeamAssignmentType teamAssignment;
|
||||||
|
private final ScoringProfile scoring;
|
||||||
|
|
||||||
|
private DqflPhase(final Builder builder)
|
||||||
|
{
|
||||||
|
this.phaseType = Objects.requireNonNull(builder.phaseType, "phaseType must not be null");
|
||||||
|
this.questionSet = builder.questionSet;
|
||||||
|
this.cycles = builder.cycles;
|
||||||
|
this.turnsPerCycle = builder.turnsPerCycle;
|
||||||
|
this.turnOrder = builder.turnOrder;
|
||||||
|
this.pickMode = builder.pickMode;
|
||||||
|
this.teamMode = builder.teamMode;
|
||||||
|
this.teamAssignment = builder.teamAssignment;
|
||||||
|
this.scoring = builder.scoring;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PhaseType getPhaseType()
|
||||||
|
{
|
||||||
|
return this.phaseType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public QuestionSetType getQuestionSet()
|
||||||
|
{
|
||||||
|
return this.questionSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getCycles()
|
||||||
|
{
|
||||||
|
return this.cycles;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getTurnsPerCycle()
|
||||||
|
{
|
||||||
|
return this.turnsPerCycle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TurnOrderType getTurnOrder()
|
||||||
|
{
|
||||||
|
return this.turnOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PickModeType getPickMode()
|
||||||
|
{
|
||||||
|
return this.pickMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TeamModeType getTeamMode()
|
||||||
|
{
|
||||||
|
return this.teamMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TeamAssignmentType getTeamAssignment()
|
||||||
|
{
|
||||||
|
return this.teamAssignment;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ScoringProfile getScoring()
|
||||||
|
{
|
||||||
|
return this.scoring;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
return "PHASE " + this.phaseType
|
||||||
|
+ " [questionSet=" + this.questionSet
|
||||||
|
+ ", teamMode=" + this.teamMode
|
||||||
|
+ ", scoring=" + this.scoring + "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// Builder
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
public static Builder builder(final PhaseType phaseType)
|
||||||
|
{
|
||||||
|
return new Builder(phaseType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class Builder
|
||||||
|
{
|
||||||
|
private final PhaseType phaseType;
|
||||||
|
private QuestionSetType questionSet;
|
||||||
|
private Integer cycles;
|
||||||
|
private Integer turnsPerCycle;
|
||||||
|
private TurnOrderType turnOrder;
|
||||||
|
private PickModeType pickMode;
|
||||||
|
private TeamModeType teamMode;
|
||||||
|
private TeamAssignmentType teamAssignment;
|
||||||
|
private ScoringProfile scoring;
|
||||||
|
|
||||||
|
private Builder(final PhaseType phaseType)
|
||||||
|
{
|
||||||
|
this.phaseType = phaseType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder questionSet(final QuestionSetType questionSet)
|
||||||
|
{
|
||||||
|
this.questionSet = questionSet;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder cycles(final int cycles)
|
||||||
|
{
|
||||||
|
this.cycles = cycles;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder turnsPerCycle(final int turnsPerCycle)
|
||||||
|
{
|
||||||
|
this.turnsPerCycle = turnsPerCycle;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder turnOrder(final TurnOrderType turnOrder)
|
||||||
|
{
|
||||||
|
this.turnOrder = turnOrder;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder pickMode(final PickModeType pickMode)
|
||||||
|
{
|
||||||
|
this.pickMode = pickMode;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder teamMode(final TeamModeType teamMode)
|
||||||
|
{
|
||||||
|
this.teamMode = teamMode;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder teamAssignment(final TeamAssignmentType teamAssignment)
|
||||||
|
{
|
||||||
|
this.teamAssignment = teamAssignment;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder scoring(final ScoringProfile scoring)
|
||||||
|
{
|
||||||
|
this.scoring = scoring;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DqflPhase build()
|
||||||
|
{
|
||||||
|
return new DqflPhase(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
55
src/main/java/de/dogfire/dqfl/model/DqflPool.java
Normal file
55
src/main/java/de/dogfire/dqfl/model/DqflPool.java
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
|
||||||
|
package de.dogfire.dqfl.model;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repräsentiert einen POOL-Block im DQFL-Dokument.
|
||||||
|
*/
|
||||||
|
public final class DqflPool
|
||||||
|
{
|
||||||
|
private final String alias;
|
||||||
|
private final String sourceAlias;
|
||||||
|
private final String remoteId;
|
||||||
|
private final String title;
|
||||||
|
|
||||||
|
public DqflPool(
|
||||||
|
final String alias,
|
||||||
|
final String sourceAlias,
|
||||||
|
final String remoteId,
|
||||||
|
final String title)
|
||||||
|
{
|
||||||
|
this.alias = Objects.requireNonNull(alias, "alias must not be null");
|
||||||
|
this.sourceAlias = Objects.requireNonNull(sourceAlias, "sourceAlias must not be null");
|
||||||
|
this.remoteId = Objects.requireNonNull(remoteId, "remoteId must not be null");
|
||||||
|
this.title = title; // optional gemäß Spec
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAlias()
|
||||||
|
{
|
||||||
|
return this.alias;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSourceAlias()
|
||||||
|
{
|
||||||
|
return this.sourceAlias;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRemoteId()
|
||||||
|
{
|
||||||
|
return this.remoteId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Kann null sein — Titel ist optional. */
|
||||||
|
public String getTitle()
|
||||||
|
{
|
||||||
|
return this.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
return "POOL " + this.alias + " [source=" + this.sourceAlias
|
||||||
|
+ ", remoteId=" + this.remoteId + "]";
|
||||||
|
}
|
||||||
|
}
|
||||||
86
src/main/java/de/dogfire/dqfl/model/DqflRound.java
Normal file
86
src/main/java/de/dogfire/dqfl/model/DqflRound.java
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
|
||||||
|
package de.dogfire.dqfl.model;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repräsentiert einen ROUND-Block auf Root-Ebene.
|
||||||
|
*/
|
||||||
|
public final class DqflRound
|
||||||
|
{
|
||||||
|
private final String alias;
|
||||||
|
private final String title;
|
||||||
|
private final List<DqflCategory> categories;
|
||||||
|
|
||||||
|
private DqflRound(final Builder builder)
|
||||||
|
{
|
||||||
|
this.alias = Objects.requireNonNull(builder.alias, "alias must not be null");
|
||||||
|
this.title = builder.title;
|
||||||
|
this.categories = Collections.unmodifiableList(new ArrayList<>(builder.categories));
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAlias()
|
||||||
|
{
|
||||||
|
return this.alias;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Kann null sein — TITLE ist optional. */
|
||||||
|
public String getTitle()
|
||||||
|
{
|
||||||
|
return this.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<DqflCategory> getCategories()
|
||||||
|
{
|
||||||
|
return this.categories;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
return "ROUND " + this.alias
|
||||||
|
+ " [title=" + this.title
|
||||||
|
+ ", categories=" + this.categories.size() + "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// Builder
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
public static Builder builder(final String alias)
|
||||||
|
{
|
||||||
|
return new Builder(alias);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class Builder
|
||||||
|
{
|
||||||
|
private final String alias;
|
||||||
|
private String title;
|
||||||
|
private final List<DqflCategory> categories = new ArrayList<>();
|
||||||
|
|
||||||
|
private Builder(final String alias)
|
||||||
|
{
|
||||||
|
this.alias = alias;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder title(final String title)
|
||||||
|
{
|
||||||
|
this.title = title;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder addCategory(final DqflCategory category)
|
||||||
|
{
|
||||||
|
this.categories.add(Objects.requireNonNull(category));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DqflRound build()
|
||||||
|
{
|
||||||
|
return new DqflRound(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
42
src/main/java/de/dogfire/dqfl/model/DqflSource.java
Normal file
42
src/main/java/de/dogfire/dqfl/model/DqflSource.java
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
|
||||||
|
package de.dogfire.dqfl.model;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repräsentiert einen SOURCE-Block im DQFL-Dokument.
|
||||||
|
*/
|
||||||
|
public final class DqflSource
|
||||||
|
{
|
||||||
|
private final String alias;
|
||||||
|
private final SourceType type;
|
||||||
|
private final String connection;
|
||||||
|
|
||||||
|
public DqflSource(final String alias, final SourceType type, final String connection)
|
||||||
|
{
|
||||||
|
this.alias = Objects.requireNonNull(alias, "alias must not be null");
|
||||||
|
this.type = Objects.requireNonNull(type, "type must not be null");
|
||||||
|
this.connection = Objects.requireNonNull(connection, "connection must not be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAlias()
|
||||||
|
{
|
||||||
|
return this.alias;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SourceType getType()
|
||||||
|
{
|
||||||
|
return this.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getConnection()
|
||||||
|
{
|
||||||
|
return this.connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
return "SOURCE " + this.alias + " [" + this.type + " -> " + this.connection + "]";
|
||||||
|
}
|
||||||
|
}
|
||||||
96
src/main/java/de/dogfire/dqfl/model/GridSize.java
Normal file
96
src/main/java/de/dogfire/dqfl/model/GridSize.java
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
|
||||||
|
package de.dogfire.dqfl.model;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repräsentiert eine SIZE-Angabe bei GRID_2D Layout.
|
||||||
|
* Format: rows x cols (z.B. "6x6").
|
||||||
|
*/
|
||||||
|
public final class GridSize
|
||||||
|
{
|
||||||
|
private final int rows;
|
||||||
|
private final int cols;
|
||||||
|
|
||||||
|
public GridSize(final int rows, final int cols)
|
||||||
|
{
|
||||||
|
if(rows <= 0 || cols <= 0)
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"rows and cols must be positive integers, got " + rows + "x" + cols);
|
||||||
|
}
|
||||||
|
this.rows = rows;
|
||||||
|
this.cols = cols;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getRows()
|
||||||
|
{
|
||||||
|
return this.rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCols()
|
||||||
|
{
|
||||||
|
return this.cols;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTotalCells()
|
||||||
|
{
|
||||||
|
return this.rows * this.cols;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parst eine SIZE-Angabe im Format "NxM".
|
||||||
|
*
|
||||||
|
* @return GridSize oder null bei ungültigem Format
|
||||||
|
*/
|
||||||
|
public static GridSize parse(final String value)
|
||||||
|
{
|
||||||
|
if(value == null || value.isBlank())
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final String[] parts = value.split("x", 2);
|
||||||
|
if(parts.length != 2)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
final int rows = Integer.parseInt(parts[0].trim());
|
||||||
|
final int cols = Integer.parseInt(parts[1].trim());
|
||||||
|
return new GridSize(rows, cols);
|
||||||
|
}
|
||||||
|
catch(final NumberFormatException e)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
return this.rows + "x" + this.cols;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(final Object o)
|
||||||
|
{
|
||||||
|
if(this == o)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if(!(o instanceof final GridSize that))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return this.rows == that.rows && this.cols == that.cols;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode()
|
||||||
|
{
|
||||||
|
return Objects.hash(this.rows, this.cols);
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/main/java/de/dogfire/dqfl/model/LayoutType.java
Normal file
23
src/main/java/de/dogfire/dqfl/model/LayoutType.java
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
|
||||||
|
package de.dogfire.dqfl.model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Layout-Typen gemäß DQFL V1.
|
||||||
|
* V1 unterstützt ausschließlich GRID_2D.
|
||||||
|
*/
|
||||||
|
public enum LayoutType
|
||||||
|
{
|
||||||
|
GRID_2D;
|
||||||
|
|
||||||
|
public static LayoutType parse(final String value)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return LayoutType.valueOf(value);
|
||||||
|
}
|
||||||
|
catch(final IllegalArgumentException e)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/main/java/de/dogfire/dqfl/model/PhaseType.java
Normal file
23
src/main/java/de/dogfire/dqfl/model/PhaseType.java
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
|
||||||
|
package de.dogfire.dqfl.model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Phase-Typen gemäß DQFL V1: NORMAL und BOSS.
|
||||||
|
*/
|
||||||
|
public enum PhaseType
|
||||||
|
{
|
||||||
|
NORMAL,
|
||||||
|
BOSS;
|
||||||
|
|
||||||
|
public static PhaseType parse(final String value)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return PhaseType.valueOf(value);
|
||||||
|
}
|
||||||
|
catch(final IllegalArgumentException e)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/main/java/de/dogfire/dqfl/model/PickModeType.java
Normal file
23
src/main/java/de/dogfire/dqfl/model/PickModeType.java
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
|
||||||
|
package de.dogfire.dqfl.model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PICK_MODE Werte gemäß DQFL V1.
|
||||||
|
* V1 unterstützt ausschließlich FREE_UNANSWERED.
|
||||||
|
*/
|
||||||
|
public enum PickModeType
|
||||||
|
{
|
||||||
|
FREE_UNANSWERED;
|
||||||
|
|
||||||
|
public static PickModeType parse(final String value)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return PickModeType.valueOf(value);
|
||||||
|
}
|
||||||
|
catch(final IllegalArgumentException e)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/main/java/de/dogfire/dqfl/model/QuestionSetType.java
Normal file
23
src/main/java/de/dogfire/dqfl/model/QuestionSetType.java
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
|
||||||
|
package de.dogfire.dqfl.model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* QUESTION_SET Werte gemäß DQFL V1.
|
||||||
|
*/
|
||||||
|
public enum QuestionSetType
|
||||||
|
{
|
||||||
|
NORMAL,
|
||||||
|
BOSS;
|
||||||
|
|
||||||
|
public static QuestionSetType parse(final String value)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return QuestionSetType.valueOf(value);
|
||||||
|
}
|
||||||
|
catch(final IllegalArgumentException e)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
45
src/main/java/de/dogfire/dqfl/model/ScoringProfile.java
Normal file
45
src/main/java/de/dogfire/dqfl/model/ScoringProfile.java
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
|
||||||
|
package de.dogfire.dqfl.model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vordefinierte Scoring-Profile gemäß DQFL V1.
|
||||||
|
*/
|
||||||
|
public enum ScoringProfile
|
||||||
|
{
|
||||||
|
/** Richtige Antwort +1, falsche Antwort 0. */
|
||||||
|
SOLO_PLUS1_NO_PENALTY(1, 0),
|
||||||
|
|
||||||
|
/** Jeder Spieler eines richtigen Teams erhält +2; Teams werden unabhängig ausgewertet. */
|
||||||
|
BOSS_2V2_PLUS2_PER_CORRECT_PLAYER(2, 0);
|
||||||
|
|
||||||
|
private final int pointsCorrect;
|
||||||
|
private final int pointsWrong;
|
||||||
|
|
||||||
|
ScoringProfile(final int pointsCorrect, final int pointsWrong)
|
||||||
|
{
|
||||||
|
this.pointsCorrect = pointsCorrect;
|
||||||
|
this.pointsWrong = pointsWrong;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getPointsCorrect()
|
||||||
|
{
|
||||||
|
return this.pointsCorrect;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getPointsWrong()
|
||||||
|
{
|
||||||
|
return this.pointsWrong;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ScoringProfile parse(final String value)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return ScoringProfile.valueOf(value);
|
||||||
|
}
|
||||||
|
catch(final IllegalArgumentException e)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/main/java/de/dogfire/dqfl/model/SourceType.java
Normal file
22
src/main/java/de/dogfire/dqfl/model/SourceType.java
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
|
||||||
|
package de.dogfire.dqfl.model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SOURCE TYPE Werte gemäß DQFL V1.
|
||||||
|
*/
|
||||||
|
public enum SourceType
|
||||||
|
{
|
||||||
|
REST;
|
||||||
|
|
||||||
|
public static SourceType parse(final String value)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return SourceType.valueOf(value);
|
||||||
|
}
|
||||||
|
catch(final IllegalArgumentException e)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/main/java/de/dogfire/dqfl/model/TeamAssignmentType.java
Normal file
23
src/main/java/de/dogfire/dqfl/model/TeamAssignmentType.java
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
|
||||||
|
package de.dogfire.dqfl.model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TEAM_ASSIGNMENT Werte gemäß DQFL V1.
|
||||||
|
* V1 unterstützt ausschließlich RANDOM.
|
||||||
|
*/
|
||||||
|
public enum TeamAssignmentType
|
||||||
|
{
|
||||||
|
RANDOM;
|
||||||
|
|
||||||
|
public static TeamAssignmentType parse(final String value)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return TeamAssignmentType.valueOf(value);
|
||||||
|
}
|
||||||
|
catch(final IllegalArgumentException e)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/main/java/de/dogfire/dqfl/model/TeamModeType.java
Normal file
23
src/main/java/de/dogfire/dqfl/model/TeamModeType.java
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
|
||||||
|
package de.dogfire.dqfl.model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TEAM_MODE Werte gemäß DQFL V1.
|
||||||
|
*/
|
||||||
|
public enum TeamModeType
|
||||||
|
{
|
||||||
|
SOLO,
|
||||||
|
TWO_VS_TWO;
|
||||||
|
|
||||||
|
public static TeamModeType parse(final String value)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return TeamModeType.valueOf(value);
|
||||||
|
}
|
||||||
|
catch(final IllegalArgumentException e)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/main/java/de/dogfire/dqfl/model/TurnOrderType.java
Normal file
23
src/main/java/de/dogfire/dqfl/model/TurnOrderType.java
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
|
||||||
|
package de.dogfire.dqfl.model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TURN_ORDER Werte gemäß DQFL V1.
|
||||||
|
* V1 unterstützt ausschließlich ROTATING_START.
|
||||||
|
*/
|
||||||
|
public enum TurnOrderType
|
||||||
|
{
|
||||||
|
ROTATING_START;
|
||||||
|
|
||||||
|
public static TurnOrderType parse(final String value)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return TurnOrderType.valueOf(value);
|
||||||
|
}
|
||||||
|
catch(final IllegalArgumentException e)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
673
src/main/java/de/dogfire/dqfl/parser/DqflParser.java
Normal file
673
src/main/java/de/dogfire/dqfl/parser/DqflParser.java
Normal file
@@ -0,0 +1,673 @@
|
|||||||
|
package de.dogfire.dqfl.parser;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import de.dogfire.dqfl.error.DqflError;
|
||||||
|
import de.dogfire.dqfl.error.DqflParseException;
|
||||||
|
import de.dogfire.dqfl.model.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parser für DQFL V1 Dokumente.
|
||||||
|
* Liest ein einrückungssensitives Textformat und erzeugt einen {@link DqflDocument} AST.
|
||||||
|
*/
|
||||||
|
public final class DqflParser
|
||||||
|
{
|
||||||
|
private final List<DqflError> errors = new ArrayList<>();
|
||||||
|
|
||||||
|
private List<ParsedLine> lines;
|
||||||
|
private int cursor;
|
||||||
|
private final Set<String> seenSourceAliases = new HashSet<>();
|
||||||
|
private final Set<String> seenPoolAliases = new HashSet<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parst den gegebenen DQFL-Text und gibt ein DqflDocument zurück.
|
||||||
|
*
|
||||||
|
* @throws DqflParseException wenn fatale Parse-Fehler auftreten
|
||||||
|
*/
|
||||||
|
public DqflDocument parse(final String input) throws DqflParseException
|
||||||
|
{
|
||||||
|
this.errors.clear();
|
||||||
|
this.cursor = 0;
|
||||||
|
this.seenSourceAliases.clear();
|
||||||
|
this.seenPoolAliases.clear();
|
||||||
|
|
||||||
|
if(input == null || input.isBlank())
|
||||||
|
{
|
||||||
|
throw new DqflParseException("Input is empty or null");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lines = this.tokenize(input);
|
||||||
|
|
||||||
|
if(this.lines.isEmpty())
|
||||||
|
{
|
||||||
|
throw new DqflParseException("No parseable lines found");
|
||||||
|
}
|
||||||
|
|
||||||
|
final DqflDocument.Builder docBuilder = DqflDocument.builder();
|
||||||
|
boolean hasVersion = false;
|
||||||
|
boolean hasFlow = false;
|
||||||
|
|
||||||
|
while(this.cursor < this.lines.size())
|
||||||
|
{
|
||||||
|
final ParsedLine line = this.lines.get(this.cursor);
|
||||||
|
|
||||||
|
if(line.indent != 0)
|
||||||
|
{
|
||||||
|
this.addError(line, "Unexpected indented line at root level");
|
||||||
|
this.cursor++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch(line.keyword)
|
||||||
|
{
|
||||||
|
case "DQFL_VERSION" -> {
|
||||||
|
if(hasVersion)
|
||||||
|
{
|
||||||
|
this.addError(line, "DQFL_VERSION must appear exactly once");
|
||||||
|
}
|
||||||
|
else if(line.value == null || line.value.isBlank())
|
||||||
|
{
|
||||||
|
this.addError(line, "DQFL_VERSION requires a value");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
docBuilder.dqflVersion(line.value);
|
||||||
|
hasVersion = true;
|
||||||
|
}
|
||||||
|
this.cursor++;
|
||||||
|
}
|
||||||
|
case "FLOW" -> {
|
||||||
|
if(hasFlow)
|
||||||
|
{
|
||||||
|
this.addError(line, "FLOW must appear exactly once");
|
||||||
|
}
|
||||||
|
else if(line.value == null || line.value.isBlank())
|
||||||
|
{
|
||||||
|
this.addError(line, "FLOW requires a value");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
docBuilder.flowName(this.unquote(line.value));
|
||||||
|
hasFlow = true;
|
||||||
|
}
|
||||||
|
this.cursor++;
|
||||||
|
}
|
||||||
|
case "SCRIPT_REVISION" -> {
|
||||||
|
docBuilder.scriptRevision(this.parseIntSafe(line, line.value));
|
||||||
|
this.cursor++;
|
||||||
|
}
|
||||||
|
case "SOURCE" -> {
|
||||||
|
final DqflSource source = this.parseSource(line);
|
||||||
|
if(source != null)
|
||||||
|
{
|
||||||
|
docBuilder.addSource(source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "POOL" -> {
|
||||||
|
final DqflPool pool = this.parsePool(line);
|
||||||
|
if(pool != null)
|
||||||
|
{
|
||||||
|
docBuilder.addPool(pool);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "ROUND" -> {
|
||||||
|
final DqflRound round = this.parseRound(line);
|
||||||
|
if(round != null)
|
||||||
|
{
|
||||||
|
docBuilder.addRound(round);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default -> {
|
||||||
|
this.addError(line, "Unknown root keyword: " + line.keyword);
|
||||||
|
this.cursor++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!hasVersion)
|
||||||
|
{
|
||||||
|
this.errors.add(DqflError.error("DQFL_VERSION is required"));
|
||||||
|
}
|
||||||
|
if(!hasFlow)
|
||||||
|
{
|
||||||
|
this.errors.add(DqflError.error("FLOW is required"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.errors.stream().anyMatch(DqflError::isError))
|
||||||
|
{
|
||||||
|
throw new DqflParseException(
|
||||||
|
"DQFL parsing failed with " + this.errors.size() + " error(s)",
|
||||||
|
this.errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
return docBuilder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<DqflError> getWarnings()
|
||||||
|
{
|
||||||
|
return this.errors.stream().filter(e -> !e.isError()).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private DqflSource parseSource(final ParsedLine headerLine)
|
||||||
|
{
|
||||||
|
final String alias = headerLine.value;
|
||||||
|
if(alias == null || alias.isBlank())
|
||||||
|
{
|
||||||
|
this.addError(headerLine, "SOURCE requires an alias");
|
||||||
|
this.cursor++;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if(!this.seenSourceAliases.add(alias))
|
||||||
|
{
|
||||||
|
this.addError(headerLine, "Duplicate SOURCE alias: " + alias);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cursor++;
|
||||||
|
|
||||||
|
String type = null;
|
||||||
|
String connection = null;
|
||||||
|
|
||||||
|
while(this.cursor < this.lines.size())
|
||||||
|
{
|
||||||
|
final ParsedLine child = this.lines.get(this.cursor);
|
||||||
|
if(child.indent <= headerLine.indent)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch(child.keyword)
|
||||||
|
{
|
||||||
|
case "TYPE" -> type = child.value;
|
||||||
|
case "CONNECTION" -> connection = child.value;
|
||||||
|
default -> this.addError(child,
|
||||||
|
"Unknown keyword in SOURCE block: " + child.keyword);
|
||||||
|
}
|
||||||
|
this.cursor++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(type == null || connection == null)
|
||||||
|
{
|
||||||
|
this.addError(headerLine,
|
||||||
|
"SOURCE " + alias + " requires both TYPE and CONNECTION");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final SourceType sourceType = SourceType.parse(type);
|
||||||
|
if(sourceType == null)
|
||||||
|
{
|
||||||
|
this.addError(headerLine, "Unknown SOURCE TYPE: " + type);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DqflSource(alias, sourceType, connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DqflPool parsePool(final ParsedLine headerLine)
|
||||||
|
{
|
||||||
|
final String alias = headerLine.value;
|
||||||
|
if(alias == null || alias.isBlank())
|
||||||
|
{
|
||||||
|
this.addError(headerLine, "POOL requires an alias");
|
||||||
|
this.cursor++;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if(!this.seenPoolAliases.add(alias))
|
||||||
|
{
|
||||||
|
this.addError(headerLine, "Duplicate POOL alias: " + alias);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cursor++;
|
||||||
|
|
||||||
|
String source = null;
|
||||||
|
String remoteId = null;
|
||||||
|
String title = null;
|
||||||
|
|
||||||
|
while(this.cursor < this.lines.size())
|
||||||
|
{
|
||||||
|
final ParsedLine child = this.lines.get(this.cursor);
|
||||||
|
if(child.indent <= headerLine.indent)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch(child.keyword)
|
||||||
|
{
|
||||||
|
case "SOURCE" -> source = child.value;
|
||||||
|
case "REMOTE_ID" -> remoteId = this.unquote(child.value);
|
||||||
|
case "TITLE" -> title = this.unquote(child.value);
|
||||||
|
default -> this.addError(child,
|
||||||
|
"Unknown keyword in POOL block: " + child.keyword);
|
||||||
|
}
|
||||||
|
this.cursor++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(source == null || remoteId == null)
|
||||||
|
{
|
||||||
|
this.addError(headerLine,
|
||||||
|
"POOL " + alias + " requires both SOURCE and REMOTE_ID");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DqflPool(alias, source, remoteId, title);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DqflRound parseRound(final ParsedLine headerLine)
|
||||||
|
{
|
||||||
|
final String alias = headerLine.value;
|
||||||
|
if(alias == null || alias.isBlank())
|
||||||
|
{
|
||||||
|
this.addError(headerLine, "ROUND requires an alias");
|
||||||
|
this.cursor++;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cursor++;
|
||||||
|
final DqflRound.Builder roundBuilder = DqflRound.builder(alias);
|
||||||
|
|
||||||
|
while(this.cursor < this.lines.size())
|
||||||
|
{
|
||||||
|
final ParsedLine child = this.lines.get(this.cursor);
|
||||||
|
if(child.indent <= headerLine.indent)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch(child.keyword)
|
||||||
|
{
|
||||||
|
case "TITLE" -> {
|
||||||
|
roundBuilder.title(this.unquote(child.value));
|
||||||
|
this.cursor++;
|
||||||
|
}
|
||||||
|
case "CATEGORY" -> {
|
||||||
|
final DqflCategory category = this.parseCategory(child);
|
||||||
|
if(category != null)
|
||||||
|
{
|
||||||
|
roundBuilder.addCategory(category);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default -> {
|
||||||
|
this.addError(child,
|
||||||
|
"Unknown keyword in ROUND block: " + child.keyword);
|
||||||
|
this.cursor++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return roundBuilder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private DqflCategory parseCategory(final ParsedLine headerLine)
|
||||||
|
{
|
||||||
|
final String alias = headerLine.value;
|
||||||
|
if(alias == null || alias.isBlank())
|
||||||
|
{
|
||||||
|
this.addError(headerLine, "CATEGORY requires an alias");
|
||||||
|
this.cursor++;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cursor++;
|
||||||
|
final DqflCategory.Builder catBuilder = DqflCategory.builder(alias);
|
||||||
|
|
||||||
|
while(this.cursor < this.lines.size())
|
||||||
|
{
|
||||||
|
final ParsedLine child = this.lines.get(this.cursor);
|
||||||
|
if(child.indent <= headerLine.indent)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch(child.keyword)
|
||||||
|
{
|
||||||
|
case "TITLE" -> {
|
||||||
|
catBuilder.title(this.unquote(child.value));
|
||||||
|
this.cursor++;
|
||||||
|
}
|
||||||
|
case "USE_POOL" -> {
|
||||||
|
catBuilder.usePool(child.value);
|
||||||
|
this.cursor++;
|
||||||
|
}
|
||||||
|
case "LAYOUT" -> {
|
||||||
|
final LayoutType lt = LayoutType.parse(child.value);
|
||||||
|
if(lt == null)
|
||||||
|
{
|
||||||
|
this.addError(child, "Unknown LAYOUT type: " + child.value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
catBuilder.layout(lt);
|
||||||
|
}
|
||||||
|
this.cursor++;
|
||||||
|
}
|
||||||
|
case "SIZE" -> {
|
||||||
|
final GridSize gs = GridSize.parse(child.value);
|
||||||
|
if(gs == null)
|
||||||
|
{
|
||||||
|
this.addError(child,
|
||||||
|
"Invalid SIZE format: " + child.value + " (expected NxM)");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
catBuilder.size(gs);
|
||||||
|
}
|
||||||
|
this.cursor++;
|
||||||
|
}
|
||||||
|
case "PHASE" -> {
|
||||||
|
final DqflPhase phase = this.parsePhase(child);
|
||||||
|
if(phase != null)
|
||||||
|
{
|
||||||
|
catBuilder.addPhase(phase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default -> {
|
||||||
|
this.addError(child,
|
||||||
|
"Unknown keyword in CATEGORY block: " + child.keyword);
|
||||||
|
this.cursor++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return catBuilder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private DqflPhase parsePhase(final ParsedLine headerLine)
|
||||||
|
{
|
||||||
|
final PhaseType phaseType = PhaseType.parse(headerLine.value);
|
||||||
|
if(phaseType == null)
|
||||||
|
{
|
||||||
|
this.addError(headerLine, "Unknown PHASE type: " + headerLine.value);
|
||||||
|
this.cursor++;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cursor++;
|
||||||
|
final DqflPhase.Builder phaseBuilder = DqflPhase.builder(phaseType);
|
||||||
|
|
||||||
|
while(this.cursor < this.lines.size())
|
||||||
|
{
|
||||||
|
final ParsedLine child = this.lines.get(this.cursor);
|
||||||
|
if(child.indent <= headerLine.indent)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch(child.keyword)
|
||||||
|
{
|
||||||
|
case "QUESTION_SET" -> {
|
||||||
|
final QuestionSetType qs = QuestionSetType.parse(child.value);
|
||||||
|
if(qs == null)
|
||||||
|
{
|
||||||
|
this.addError(child, "Unknown QUESTION_SET: " + child.value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
phaseBuilder.questionSet(qs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "CYCLES" -> {
|
||||||
|
final Integer v = this.parseIntSafe(child, child.value);
|
||||||
|
if(v != null)
|
||||||
|
{
|
||||||
|
phaseBuilder.cycles(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "TURNS_PER_CYCLE" -> {
|
||||||
|
final Integer v = this.parseIntSafe(child, child.value);
|
||||||
|
if(v != null)
|
||||||
|
{
|
||||||
|
phaseBuilder.turnsPerCycle(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "TURN_ORDER" -> {
|
||||||
|
final TurnOrderType to = TurnOrderType.parse(child.value);
|
||||||
|
if(to == null)
|
||||||
|
{
|
||||||
|
this.addError(child, "Unknown TURN_ORDER: " + child.value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
phaseBuilder.turnOrder(to);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "PICK_MODE" -> {
|
||||||
|
final PickModeType pm = PickModeType.parse(child.value);
|
||||||
|
if(pm == null)
|
||||||
|
{
|
||||||
|
this.addError(child, "Unknown PICK_MODE: " + child.value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
phaseBuilder.pickMode(pm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "TEAM_MODE" -> {
|
||||||
|
final TeamModeType tm = TeamModeType.parse(child.value);
|
||||||
|
if(tm == null)
|
||||||
|
{
|
||||||
|
this.addError(child, "Unknown TEAM_MODE: " + child.value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
phaseBuilder.teamMode(tm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "TEAM_ASSIGNMENT" -> {
|
||||||
|
final TeamAssignmentType ta = TeamAssignmentType.parse(child.value);
|
||||||
|
if(ta == null)
|
||||||
|
{
|
||||||
|
this.addError(child, "Unknown TEAM_ASSIGNMENT: " + child.value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
phaseBuilder.teamAssignment(ta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "SCORING" -> {
|
||||||
|
final ScoringProfile sp = ScoringProfile.parse(child.value);
|
||||||
|
if(sp == null)
|
||||||
|
{
|
||||||
|
this.addError(child, "Unknown SCORING profile: " + child.value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
phaseBuilder.scoring(sp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default -> this.addError(child,
|
||||||
|
"Unknown keyword in PHASE block: " + child.keyword);
|
||||||
|
}
|
||||||
|
this.cursor++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return phaseBuilder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ParsedLine> tokenize(final String input)
|
||||||
|
{
|
||||||
|
final List<ParsedLine> result = new ArrayList<>();
|
||||||
|
final String[] rawLines = input.split("\\r?\\n");
|
||||||
|
|
||||||
|
Character indentChar = null;
|
||||||
|
int indentUnit = -1;
|
||||||
|
int previousIndent = 0;
|
||||||
|
|
||||||
|
for(int i = 0; i < rawLines.length; i++)
|
||||||
|
{
|
||||||
|
final String raw = rawLines[i];
|
||||||
|
final int lineNumber = i + 1;
|
||||||
|
final String trimmed = raw.trim();
|
||||||
|
|
||||||
|
if(trimmed.isEmpty() || trimmed.startsWith("#"))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int contentStart = this.findFirstNonWhitespace(raw);
|
||||||
|
if(contentStart < 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final String indentation = raw.substring(0, contentStart);
|
||||||
|
|
||||||
|
if(!indentation.isEmpty())
|
||||||
|
{
|
||||||
|
final boolean hasSpaces = indentation.indexOf(' ') >= 0;
|
||||||
|
final boolean hasTabs = indentation.indexOf('\t') >= 0;
|
||||||
|
if(hasSpaces && hasTabs)
|
||||||
|
{
|
||||||
|
this.errors.add(DqflError.error(lineNumber,
|
||||||
|
"Mixed tabs and spaces in indentation"));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final char lineIndentChar = indentation.charAt(0);
|
||||||
|
if(indentChar == null)
|
||||||
|
{
|
||||||
|
indentChar = lineIndentChar;
|
||||||
|
}
|
||||||
|
else if(lineIndentChar != indentChar)
|
||||||
|
{
|
||||||
|
this.errors.add(DqflError.error(lineNumber,
|
||||||
|
"Inconsistent indentation character; use either tabs or spaces consistently"));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int indent = 0;
|
||||||
|
if(!indentation.isEmpty())
|
||||||
|
{
|
||||||
|
if(indentUnit < 0)
|
||||||
|
{
|
||||||
|
indentUnit = indentation.length();
|
||||||
|
if(indentUnit <= 0)
|
||||||
|
{
|
||||||
|
this.errors.add(DqflError.error(lineNumber, "Invalid indentation width"));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(indentation.length() % indentUnit != 0)
|
||||||
|
{
|
||||||
|
this.errors.add(DqflError.error(lineNumber,
|
||||||
|
"Indentation does not match the configured indentation width of " + indentUnit));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
indent = indentation.length() / indentUnit;
|
||||||
|
if(indent > previousIndent + 1)
|
||||||
|
{
|
||||||
|
this.errors.add(DqflError.error(lineNumber,
|
||||||
|
"Indentation may only increase by one level at a time"));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
indent = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int spaceIdx = trimmed.indexOf(' ');
|
||||||
|
final String keyword;
|
||||||
|
final String value;
|
||||||
|
|
||||||
|
if(spaceIdx < 0)
|
||||||
|
{
|
||||||
|
keyword = trimmed;
|
||||||
|
value = null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
keyword = trimmed.substring(0, spaceIdx);
|
||||||
|
value = trimmed.substring(spaceIdx + 1).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
result.add(new ParsedLine(lineNumber, indent, keyword, value));
|
||||||
|
previousIndent = indent;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int findFirstNonWhitespace(final String raw)
|
||||||
|
{
|
||||||
|
for(int i = 0; i < raw.length(); i++)
|
||||||
|
{
|
||||||
|
final char ch = raw.charAt(i);
|
||||||
|
if(ch != ' ' && ch != '\t')
|
||||||
|
{
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String unquote(final String value)
|
||||||
|
{
|
||||||
|
if(value == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String v = value.trim();
|
||||||
|
if(v.length() >= 2 && v.startsWith("\"") && v.endsWith("\""))
|
||||||
|
{
|
||||||
|
v = v.substring(1, v.length() - 1);
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer parseIntSafe(final ParsedLine line, final String value)
|
||||||
|
{
|
||||||
|
if(value == null || value.isBlank())
|
||||||
|
{
|
||||||
|
this.addError(line, line.keyword + " requires a numeric value");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
final int v = Integer.parseInt(value.trim());
|
||||||
|
if(v <= 0)
|
||||||
|
{
|
||||||
|
this.addError(line, line.keyword + " must be a positive integer, got " + v);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
catch(final NumberFormatException e)
|
||||||
|
{
|
||||||
|
this.addError(line,
|
||||||
|
line.keyword + " is not a valid integer: " + value);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addError(final ParsedLine line, final String message)
|
||||||
|
{
|
||||||
|
this.errors.add(DqflError.error(line.lineNumber, message));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class ParsedLine
|
||||||
|
{
|
||||||
|
final int lineNumber;
|
||||||
|
final int indent;
|
||||||
|
final String keyword;
|
||||||
|
final String value;
|
||||||
|
|
||||||
|
ParsedLine(final int lineNumber, final int indent, final String keyword, final String value)
|
||||||
|
{
|
||||||
|
this.lineNumber = lineNumber;
|
||||||
|
this.indent = indent;
|
||||||
|
this.keyword = keyword;
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
return "L" + this.lineNumber + " [" + this.indent + "] "
|
||||||
|
+ this.keyword + (this.value != null ? " " + this.value : "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
265
src/main/java/de/dogfire/dqfl/validator/DqflValidator.java
Normal file
265
src/main/java/de/dogfire/dqfl/validator/DqflValidator.java
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
|
||||||
|
package de.dogfire.dqfl.validator;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import de.dogfire.dqfl.error.DqflError;
|
||||||
|
import de.dogfire.dqfl.error.DqflValidationResult;
|
||||||
|
import de.dogfire.dqfl.model.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validator für DQFL V1 Dokumente.
|
||||||
|
* Prüft alle Fachregeln aus Kapitel 10 der Spezifikation.
|
||||||
|
*/
|
||||||
|
public final class DqflValidator
|
||||||
|
{
|
||||||
|
/** Empfohlenes Validierungsmuster für REMOTE_ID: ^[A-Z0-9]{8}[0-9]{10,}$ */
|
||||||
|
private static final Pattern REMOTE_ID_PATTERN =
|
||||||
|
Pattern.compile("^[A-Z0-9]{8}[0-9]{10,}$");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validiert ein geparstes DqflDocument und gibt das Ergebnis zurück.
|
||||||
|
*/
|
||||||
|
public DqflValidationResult validate(final DqflDocument document)
|
||||||
|
{
|
||||||
|
final List<DqflError> errors = new ArrayList<>();
|
||||||
|
|
||||||
|
// 1. DQFL_VERSION muss vorhanden sein
|
||||||
|
if(document.getDqflVersion() == null || document.getDqflVersion().isBlank())
|
||||||
|
{
|
||||||
|
errors.add(DqflError.error("DQFL_VERSION is required"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. FLOW muss vorhanden sein
|
||||||
|
if(document.getFlowName() == null || document.getFlowName().isBlank())
|
||||||
|
{
|
||||||
|
errors.add(DqflError.error("FLOW is required"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. SOURCE-Aliase müssen eindeutig sein (bereits durch Map im Parser sichergestellt,
|
||||||
|
// aber wir prüfen trotzdem ob Sources vorhanden sind wenn Pools sie referenzieren)
|
||||||
|
|
||||||
|
// 4. POOL-Validierung
|
||||||
|
this.validatePools(document, errors);
|
||||||
|
|
||||||
|
// 5. ROUND-Validierung
|
||||||
|
this.validateRounds(document, errors);
|
||||||
|
|
||||||
|
// 6. Alias-Eindeutigkeit über Objektarten
|
||||||
|
this.validateAliasUniqueness(document, errors);
|
||||||
|
|
||||||
|
return new DqflValidationResult(errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// Pool-Validierung
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
private void validatePools(final DqflDocument document, final List<DqflError> errors)
|
||||||
|
{
|
||||||
|
for(final DqflPool pool : document.getPools().values())
|
||||||
|
{
|
||||||
|
// SOURCE-Referenz muss existieren
|
||||||
|
if(document.getSource(pool.getSourceAlias()) == null)
|
||||||
|
{
|
||||||
|
errors.add(DqflError.error(
|
||||||
|
"POOL '" + pool.getAlias()
|
||||||
|
+ "' references unknown SOURCE '" + pool.getSourceAlias() + "'"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// REMOTE_ID Format prüfen (als Warnung, nicht als Fehler)
|
||||||
|
if(!REMOTE_ID_PATTERN.matcher(pool.getRemoteId()).matches())
|
||||||
|
{
|
||||||
|
errors.add(DqflError.warning(
|
||||||
|
"POOL '" + pool.getAlias()
|
||||||
|
+ "' REMOTE_ID '" + pool.getRemoteId()
|
||||||
|
+ "' does not match recommended pattern ^[A-Z0-9]{8}[0-9]{10,}$"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// Round-Validierung
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
private void validateRounds(final DqflDocument document, final List<DqflError> errors)
|
||||||
|
{
|
||||||
|
if(document.getRounds().isEmpty())
|
||||||
|
{
|
||||||
|
errors.add(DqflError.error("Document must contain at least one ROUND"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Set<String> roundAliases = new HashSet<>();
|
||||||
|
final Set<String> categoryAliases = new HashSet<>();
|
||||||
|
|
||||||
|
for(final DqflRound round : document.getRounds())
|
||||||
|
{
|
||||||
|
// ROUND-Alias Eindeutigkeit
|
||||||
|
if(!roundAliases.add(round.getAlias()))
|
||||||
|
{
|
||||||
|
errors.add(DqflError.error(
|
||||||
|
"Duplicate ROUND alias: " + round.getAlias()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if(round.getCategories().isEmpty())
|
||||||
|
{
|
||||||
|
errors.add(DqflError.error(
|
||||||
|
"ROUND '" + round.getAlias() + "' must contain at least one CATEGORY"));
|
||||||
|
}
|
||||||
|
|
||||||
|
for(final DqflCategory category : round.getCategories())
|
||||||
|
{
|
||||||
|
this.validateCategory(category, document, categoryAliases, errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// Category-Validierung
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
private void validateCategory(
|
||||||
|
final DqflCategory category,
|
||||||
|
final DqflDocument document,
|
||||||
|
final Set<String> categoryAliases,
|
||||||
|
final List<DqflError> errors)
|
||||||
|
{
|
||||||
|
// CATEGORY-Alias Eindeutigkeit
|
||||||
|
if(!categoryAliases.add(category.getAlias()))
|
||||||
|
{
|
||||||
|
errors.add(DqflError.error(
|
||||||
|
"Duplicate CATEGORY alias: " + category.getAlias()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jede CATEGORY muss genau einen USE_POOL enthalten
|
||||||
|
if(category.getUsePool() == null || category.getUsePool().isBlank())
|
||||||
|
{
|
||||||
|
errors.add(DqflError.error(
|
||||||
|
"CATEGORY '" + category.getAlias() + "' must have exactly one USE_POOL"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// USE_POOL muss auf existierenden POOL verweisen
|
||||||
|
if(document.getPool(category.getUsePool()) == null)
|
||||||
|
{
|
||||||
|
errors.add(DqflError.error(
|
||||||
|
"CATEGORY '" + category.getAlias()
|
||||||
|
+ "' references unknown POOL '" + category.getUsePool() + "'"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SIZE nur gültig wenn LAYOUT vorhanden
|
||||||
|
if(category.getSize() != null && category.getLayout() == null)
|
||||||
|
{
|
||||||
|
errors.add(DqflError.error(
|
||||||
|
"CATEGORY '" + category.getAlias()
|
||||||
|
+ "' has SIZE without LAYOUT"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bei GRID_2D muss SIZE vorhanden sein
|
||||||
|
if(category.getLayout() == LayoutType.GRID_2D && category.getSize() == null)
|
||||||
|
{
|
||||||
|
errors.add(DqflError.error(
|
||||||
|
"CATEGORY '" + category.getAlias()
|
||||||
|
+ "' has LAYOUT GRID_2D but no SIZE specified"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jede CATEGORY muss mindestens eine PHASE enthalten
|
||||||
|
if(category.getPhases().isEmpty())
|
||||||
|
{
|
||||||
|
errors.add(DqflError.error(
|
||||||
|
"CATEGORY '" + category.getAlias() + "' must contain at least one PHASE"));
|
||||||
|
}
|
||||||
|
|
||||||
|
for(final DqflPhase phase : category.getPhases())
|
||||||
|
{
|
||||||
|
this.validatePhase(phase, category.getAlias(), errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// Phase-Validierung
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
private void validatePhase(
|
||||||
|
final DqflPhase phase,
|
||||||
|
final String categoryAlias,
|
||||||
|
final List<DqflError> errors)
|
||||||
|
{
|
||||||
|
final String ctx = "CATEGORY '" + categoryAlias + "' PHASE " + phase.getPhaseType();
|
||||||
|
|
||||||
|
// SCORING muss gesetzt sein
|
||||||
|
if(phase.getScoring() == null)
|
||||||
|
{
|
||||||
|
errors.add(DqflError.error(ctx + " requires SCORING"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// TEAM_MODE muss gesetzt sein
|
||||||
|
if(phase.getTeamMode() == null)
|
||||||
|
{
|
||||||
|
errors.add(DqflError.error(ctx + " requires TEAM_MODE"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// NORMAL-Phase Regeln
|
||||||
|
if(phase.getPhaseType() == PhaseType.NORMAL)
|
||||||
|
{
|
||||||
|
if(phase.getCycles() == null)
|
||||||
|
{
|
||||||
|
errors.add(DqflError.warning(ctx + " has no CYCLES defined"));
|
||||||
|
}
|
||||||
|
if(phase.getTurnsPerCycle() == null)
|
||||||
|
{
|
||||||
|
errors.add(DqflError.warning(ctx + " has no TURNS_PER_CYCLE defined"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BOSS-Phase: TEAM_MODE sollte TWO_VS_TWO sein
|
||||||
|
if(phase.getPhaseType() == PhaseType.BOSS)
|
||||||
|
{
|
||||||
|
if(phase.getTeamMode() == TeamModeType.SOLO)
|
||||||
|
{
|
||||||
|
errors.add(DqflError.warning(
|
||||||
|
ctx + " is BOSS phase but TEAM_MODE is SOLO — typically TWO_VS_TWO"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if(phase.getTeamMode() == TeamModeType.TWO_VS_TWO
|
||||||
|
&& phase.getTeamAssignment() == null)
|
||||||
|
{
|
||||||
|
errors.add(DqflError.error(
|
||||||
|
ctx + " has TEAM_MODE TWO_VS_TWO but no TEAM_ASSIGNMENT"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// Alias-Eindeutigkeit (über Objektarten)
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
private void validateAliasUniqueness(
|
||||||
|
final DqflDocument document,
|
||||||
|
final List<DqflError> errors)
|
||||||
|
{
|
||||||
|
// SOURCE-Aliase eindeutig (Map-Struktur prüft bereits implizit,
|
||||||
|
// aber bei Duplikaten im Dokument wurde der erste überschrieben)
|
||||||
|
// POOL-Aliase eindeutig (gleiche Map-Logik)
|
||||||
|
|
||||||
|
// Wir prüfen explizit ob Pool-Aliase und Source-Aliase kollidieren (optional, sauber)
|
||||||
|
final Set<String> sourceAliases = document.getSources().keySet();
|
||||||
|
final Set<String> poolAliases = document.getPools().keySet();
|
||||||
|
|
||||||
|
for(final String poolAlias : poolAliases)
|
||||||
|
{
|
||||||
|
if(sourceAliases.contains(poolAlias))
|
||||||
|
{
|
||||||
|
errors.add(DqflError.warning(
|
||||||
|
"POOL alias '" + poolAlias
|
||||||
|
+ "' collides with a SOURCE alias — this may cause confusion"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
411
src/test/java/de/dogfire/dqfl/DqflTest.java
Normal file
411
src/test/java/de/dogfire/dqfl/DqflTest.java
Normal file
@@ -0,0 +1,411 @@
|
|||||||
|
|
||||||
|
package de.dogfire.dqfl;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import de.dogfire.dqfl.error.DqflParseException;
|
||||||
|
import de.dogfire.dqfl.error.DqflValidationResult;
|
||||||
|
import de.dogfire.dqfl.model.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests für die DQFL V1 Library: Parser + Validator.
|
||||||
|
*/
|
||||||
|
class DqflTest
|
||||||
|
{
|
||||||
|
// ================================================================
|
||||||
|
// Parser Tests
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Parser")
|
||||||
|
class ParserTests
|
||||||
|
{
|
||||||
|
@Test
|
||||||
|
@DisplayName("Parst das vollständige Referenzbeispiel aus der Spec")
|
||||||
|
void parseReferenceExample() throws Exception
|
||||||
|
{
|
||||||
|
final DqflDocument doc = loadReferenceDocument();
|
||||||
|
|
||||||
|
assertEquals("V1", doc.getDqflVersion());
|
||||||
|
assertEquals("Dogfire Quizabend", doc.getFlowName());
|
||||||
|
assertEquals(1, doc.getScriptRevision());
|
||||||
|
|
||||||
|
// Sources
|
||||||
|
assertEquals(1, doc.getSources().size());
|
||||||
|
final DqflSource src = doc.getSource("SRC1");
|
||||||
|
assertNotNull(src);
|
||||||
|
assertEquals(SourceType.REST, src.getType());
|
||||||
|
assertEquals("USER_POOL_MAIN", src.getConnection());
|
||||||
|
|
||||||
|
// Pools
|
||||||
|
assertEquals(2, doc.getPools().size());
|
||||||
|
final DqflPool history = doc.getPool("HISTORY");
|
||||||
|
assertNotNull(history);
|
||||||
|
assertEquals("SRC1", history.getSourceAlias());
|
||||||
|
assertEquals("AB12CD340000004711", history.getRemoteId());
|
||||||
|
assertEquals("Geschichte", history.getTitle());
|
||||||
|
|
||||||
|
final DqflPool movies = doc.getPool("MOVIES");
|
||||||
|
assertNotNull(movies);
|
||||||
|
assertEquals("ZX82LM550000009321", movies.getRemoteId());
|
||||||
|
|
||||||
|
// Rounds
|
||||||
|
assertEquals(1, doc.getRounds().size());
|
||||||
|
final DqflRound round = doc.getRounds().get(0);
|
||||||
|
assertEquals("R1", round.getAlias());
|
||||||
|
assertEquals("Rematch", round.getTitle());
|
||||||
|
|
||||||
|
// Categories
|
||||||
|
assertEquals(2, round.getCategories().size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Parst Category-Details korrekt")
|
||||||
|
void parseCategoryDetails() throws Exception
|
||||||
|
{
|
||||||
|
final DqflDocument doc = loadReferenceDocument();
|
||||||
|
final DqflCategory c1 = doc.getRounds().get(0).getCategories().get(0);
|
||||||
|
|
||||||
|
assertEquals("C1", c1.getAlias());
|
||||||
|
assertEquals("Geschichte", c1.getTitle());
|
||||||
|
assertEquals("HISTORY", c1.getUsePool());
|
||||||
|
assertEquals(LayoutType.GRID_2D, c1.getLayout());
|
||||||
|
assertEquals(new GridSize(6, 6), c1.getSize());
|
||||||
|
assertEquals(2, c1.getPhases().size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Parst NORMAL Phase korrekt")
|
||||||
|
void parseNormalPhase() throws Exception
|
||||||
|
{
|
||||||
|
final DqflDocument doc = loadReferenceDocument();
|
||||||
|
final DqflPhase normal = doc.getRounds().get(0).getCategories().get(0).getPhases().get(0);
|
||||||
|
|
||||||
|
assertEquals(PhaseType.NORMAL, normal.getPhaseType());
|
||||||
|
assertEquals(QuestionSetType.NORMAL, normal.getQuestionSet());
|
||||||
|
assertEquals(3, normal.getCycles());
|
||||||
|
assertEquals(4, normal.getTurnsPerCycle());
|
||||||
|
assertEquals(TurnOrderType.ROTATING_START, normal.getTurnOrder());
|
||||||
|
assertEquals(PickModeType.FREE_UNANSWERED, normal.getPickMode());
|
||||||
|
assertEquals(TeamModeType.SOLO, normal.getTeamMode());
|
||||||
|
assertEquals(ScoringProfile.SOLO_PLUS1_NO_PENALTY, normal.getScoring());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Parst BOSS Phase korrekt")
|
||||||
|
void parseBossPhase() throws Exception
|
||||||
|
{
|
||||||
|
final DqflDocument doc = loadReferenceDocument();
|
||||||
|
final DqflPhase boss = doc.getRounds().get(0).getCategories().get(0).getPhases().get(1);
|
||||||
|
|
||||||
|
assertEquals(PhaseType.BOSS, boss.getPhaseType());
|
||||||
|
assertEquals(QuestionSetType.BOSS, boss.getQuestionSet());
|
||||||
|
assertEquals(TeamModeType.TWO_VS_TWO, boss.getTeamMode());
|
||||||
|
assertEquals(TeamAssignmentType.RANDOM, boss.getTeamAssignment());
|
||||||
|
assertEquals(ScoringProfile.BOSS_2V2_PLUS2_PER_CORRECT_PLAYER, boss.getScoring());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("getAllCategoriesFlat liefert alle Kategorien in Dokumentreihenfolge")
|
||||||
|
void flatCategoryList() throws Exception
|
||||||
|
{
|
||||||
|
final DqflDocument doc = loadReferenceDocument();
|
||||||
|
final var flat = doc.getAllCategoriesFlat();
|
||||||
|
|
||||||
|
assertEquals(2, flat.size());
|
||||||
|
assertEquals("C1", flat.get(0).getAlias());
|
||||||
|
assertEquals("C2", flat.get(1).getAlias());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Leerer Input wirft DqflParseException")
|
||||||
|
void emptyInputThrows()
|
||||||
|
{
|
||||||
|
assertThrows(DqflParseException.class, () -> Dqfl.parse(""));
|
||||||
|
assertThrows(DqflParseException.class, () -> Dqfl.parse(" \n \n "));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Unbekanntes Root-Keyword erzeugt Fehler")
|
||||||
|
void unknownRootKeyword()
|
||||||
|
{
|
||||||
|
final String input = """
|
||||||
|
DQFL_VERSION V1
|
||||||
|
FLOW "Test"
|
||||||
|
BOGUS_KEYWORD value
|
||||||
|
""";
|
||||||
|
|
||||||
|
final DqflParseException ex = assertThrows(DqflParseException.class,
|
||||||
|
() -> Dqfl.parse(input));
|
||||||
|
|
||||||
|
assertTrue(ex.getErrors().stream()
|
||||||
|
.anyMatch(e -> e.getMessage().contains("BOGUS_KEYWORD")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// Validator Tests
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Validator")
|
||||||
|
class ValidatorTests
|
||||||
|
{
|
||||||
|
@Test
|
||||||
|
@DisplayName("Referenzbeispiel besteht die Validierung")
|
||||||
|
void referenceExampleIsValid() throws Exception
|
||||||
|
{
|
||||||
|
final DqflDocument doc = loadReferenceDocument();
|
||||||
|
final DqflValidationResult result = Dqfl.validate(doc);
|
||||||
|
|
||||||
|
assertTrue(result.isValid(),
|
||||||
|
"Reference example should be valid. Errors: " + result.getErrorsOnly());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Fehlender USE_POOL wird erkannt")
|
||||||
|
void missingUsePool() throws Exception
|
||||||
|
{
|
||||||
|
final String input = """
|
||||||
|
DQFL_VERSION V1
|
||||||
|
FLOW "Test"
|
||||||
|
SOURCE SRC1
|
||||||
|
TYPE REST
|
||||||
|
CONNECTION CONN1
|
||||||
|
POOL P1
|
||||||
|
SOURCE SRC1
|
||||||
|
REMOTE_ID "AB12CD340000004711"
|
||||||
|
ROUND R1
|
||||||
|
CATEGORY C1
|
||||||
|
LAYOUT GRID_2D
|
||||||
|
SIZE 6x6
|
||||||
|
PHASE NORMAL
|
||||||
|
TEAM_MODE SOLO
|
||||||
|
SCORING SOLO_PLUS1_NO_PENALTY
|
||||||
|
""";
|
||||||
|
|
||||||
|
final DqflDocument doc = Dqfl.parse(input);
|
||||||
|
final DqflValidationResult result = Dqfl.validate(doc);
|
||||||
|
|
||||||
|
assertFalse(result.isValid());
|
||||||
|
assertTrue(result.getErrorsOnly().stream()
|
||||||
|
.anyMatch(e -> e.getMessage().contains("USE_POOL")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("SIZE ohne LAYOUT wird erkannt")
|
||||||
|
void sizeWithoutLayout() throws Exception
|
||||||
|
{
|
||||||
|
final String input = """
|
||||||
|
DQFL_VERSION V1
|
||||||
|
FLOW "Test"
|
||||||
|
SOURCE SRC1
|
||||||
|
TYPE REST
|
||||||
|
CONNECTION CONN1
|
||||||
|
POOL P1
|
||||||
|
SOURCE SRC1
|
||||||
|
REMOTE_ID "AB12CD340000004711"
|
||||||
|
ROUND R1
|
||||||
|
CATEGORY C1
|
||||||
|
USE_POOL P1
|
||||||
|
SIZE 6x6
|
||||||
|
PHASE NORMAL
|
||||||
|
TEAM_MODE SOLO
|
||||||
|
SCORING SOLO_PLUS1_NO_PENALTY
|
||||||
|
""";
|
||||||
|
|
||||||
|
final DqflDocument doc = Dqfl.parse(input);
|
||||||
|
final DqflValidationResult result = Dqfl.validate(doc);
|
||||||
|
|
||||||
|
assertFalse(result.isValid());
|
||||||
|
assertTrue(result.getErrorsOnly().stream()
|
||||||
|
.anyMatch(e -> e.getMessage().contains("SIZE without LAYOUT")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Fehlende PHASE wird erkannt")
|
||||||
|
void missingPhase() throws Exception
|
||||||
|
{
|
||||||
|
final String input = """
|
||||||
|
DQFL_VERSION V1
|
||||||
|
FLOW "Test"
|
||||||
|
SOURCE SRC1
|
||||||
|
TYPE REST
|
||||||
|
CONNECTION CONN1
|
||||||
|
POOL P1
|
||||||
|
SOURCE SRC1
|
||||||
|
REMOTE_ID "AB12CD340000004711"
|
||||||
|
ROUND R1
|
||||||
|
CATEGORY C1
|
||||||
|
USE_POOL P1
|
||||||
|
""";
|
||||||
|
|
||||||
|
final DqflDocument doc = Dqfl.parse(input);
|
||||||
|
final DqflValidationResult result = Dqfl.validate(doc);
|
||||||
|
|
||||||
|
assertFalse(result.isValid());
|
||||||
|
assertTrue(result.getErrorsOnly().stream()
|
||||||
|
.anyMatch(e -> e.getMessage().contains("at least one PHASE")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Unbekannter POOL-Verweis wird erkannt")
|
||||||
|
void unknownPoolReference() throws Exception
|
||||||
|
{
|
||||||
|
final String input = """
|
||||||
|
DQFL_VERSION V1
|
||||||
|
FLOW "Test"
|
||||||
|
SOURCE SRC1
|
||||||
|
TYPE REST
|
||||||
|
CONNECTION CONN1
|
||||||
|
ROUND R1
|
||||||
|
CATEGORY C1
|
||||||
|
USE_POOL DOES_NOT_EXIST
|
||||||
|
PHASE NORMAL
|
||||||
|
TEAM_MODE SOLO
|
||||||
|
SCORING SOLO_PLUS1_NO_PENALTY
|
||||||
|
""";
|
||||||
|
|
||||||
|
final DqflDocument doc = Dqfl.parse(input);
|
||||||
|
final DqflValidationResult result = Dqfl.validate(doc);
|
||||||
|
|
||||||
|
assertFalse(result.isValid());
|
||||||
|
assertTrue(result.getErrorsOnly().stream()
|
||||||
|
.anyMatch(e -> e.getMessage().contains("DOES_NOT_EXIST")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("BOSS Phase ohne TEAM_ASSIGNMENT wird erkannt")
|
||||||
|
void bossWithoutTeamAssignment() throws Exception
|
||||||
|
{
|
||||||
|
final String input = """
|
||||||
|
DQFL_VERSION V1
|
||||||
|
FLOW "Test"
|
||||||
|
SOURCE SRC1
|
||||||
|
TYPE REST
|
||||||
|
CONNECTION CONN1
|
||||||
|
POOL P1
|
||||||
|
SOURCE SRC1
|
||||||
|
REMOTE_ID "AB12CD340000004711"
|
||||||
|
ROUND R1
|
||||||
|
CATEGORY C1
|
||||||
|
USE_POOL P1
|
||||||
|
PHASE BOSS
|
||||||
|
QUESTION_SET BOSS
|
||||||
|
TEAM_MODE TWO_VS_TWO
|
||||||
|
SCORING BOSS_2V2_PLUS2_PER_CORRECT_PLAYER
|
||||||
|
""";
|
||||||
|
|
||||||
|
final DqflDocument doc = Dqfl.parse(input);
|
||||||
|
final DqflValidationResult result = Dqfl.validate(doc);
|
||||||
|
|
||||||
|
assertFalse(result.isValid());
|
||||||
|
assertTrue(result.getErrorsOnly().stream()
|
||||||
|
.anyMatch(e -> e.getMessage().contains("TEAM_ASSIGNMENT")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("parseAndValidate Convenience-Methode funktioniert")
|
||||||
|
void parseAndValidateConvenience() throws Exception
|
||||||
|
{
|
||||||
|
final String input = loadReferenceString();
|
||||||
|
final DqflDocument doc = Dqfl.parseAndValidate(input);
|
||||||
|
|
||||||
|
assertNotNull(doc);
|
||||||
|
assertEquals("V1", doc.getDqflVersion());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// GridSize Tests
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("GridSize")
|
||||||
|
class GridSizeTests
|
||||||
|
{
|
||||||
|
@Test
|
||||||
|
void parseValid()
|
||||||
|
{
|
||||||
|
final GridSize gs = GridSize.parse("6x6");
|
||||||
|
assertNotNull(gs);
|
||||||
|
assertEquals(6, gs.getRows());
|
||||||
|
assertEquals(6, gs.getCols());
|
||||||
|
assertEquals(36, gs.getTotalCells());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void parseRectangular()
|
||||||
|
{
|
||||||
|
final GridSize gs = GridSize.parse("4x9");
|
||||||
|
assertNotNull(gs);
|
||||||
|
assertEquals(4, gs.getRows());
|
||||||
|
assertEquals(9, gs.getCols());
|
||||||
|
assertEquals(36, gs.getTotalCells());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void parseInvalid()
|
||||||
|
{
|
||||||
|
assertNull(GridSize.parse("abc"));
|
||||||
|
assertNull(GridSize.parse("6"));
|
||||||
|
assertNull(GridSize.parse(null));
|
||||||
|
assertNull(GridSize.parse(""));
|
||||||
|
assertNull(GridSize.parse("0x6")); // 0 ist nicht positiv
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void equality()
|
||||||
|
{
|
||||||
|
assertEquals(GridSize.parse("6x6"), GridSize.parse("6x6"));
|
||||||
|
assertNotEquals(GridSize.parse("6x6"), GridSize.parse("4x9"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// ScoringProfile Tests
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("ScoringProfile")
|
||||||
|
class ScoringProfileTests
|
||||||
|
{
|
||||||
|
@Test
|
||||||
|
void soloPoints()
|
||||||
|
{
|
||||||
|
assertEquals(1, ScoringProfile.SOLO_PLUS1_NO_PENALTY.getPointsCorrect());
|
||||||
|
assertEquals(0, ScoringProfile.SOLO_PLUS1_NO_PENALTY.getPointsWrong());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void bossPoints()
|
||||||
|
{
|
||||||
|
assertEquals(2, ScoringProfile.BOSS_2V2_PLUS2_PER_CORRECT_PLAYER.getPointsCorrect());
|
||||||
|
assertEquals(0, ScoringProfile.BOSS_2V2_PLUS2_PER_CORRECT_PLAYER.getPointsWrong());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// Helpers
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
private static DqflDocument loadReferenceDocument() throws Exception
|
||||||
|
{
|
||||||
|
return Dqfl.parse(loadReferenceString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String loadReferenceString() throws Exception
|
||||||
|
{
|
||||||
|
try(final InputStream is = DqflTest.class.getResourceAsStream("/reference-example.dqfl"))
|
||||||
|
{
|
||||||
|
assertNotNull(is, "reference-example.dqfl not found on classpath");
|
||||||
|
return new String(is.readAllBytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
56
src/test/resources/reference-example.dqfl
Normal file
56
src/test/resources/reference-example.dqfl
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
DQFL_VERSION V1
|
||||||
|
FLOW "Dogfire Quizabend"
|
||||||
|
SCRIPT_REVISION 1
|
||||||
|
|
||||||
|
SOURCE SRC1
|
||||||
|
TYPE REST
|
||||||
|
CONNECTION USER_POOL_MAIN
|
||||||
|
|
||||||
|
POOL HISTORY
|
||||||
|
SOURCE SRC1
|
||||||
|
REMOTE_ID "AB12CD340000004711"
|
||||||
|
TITLE "Geschichte"
|
||||||
|
|
||||||
|
POOL MOVIES
|
||||||
|
SOURCE SRC1
|
||||||
|
REMOTE_ID "ZX82LM550000009321"
|
||||||
|
TITLE "Filme"
|
||||||
|
|
||||||
|
ROUND R1
|
||||||
|
TITLE "Rematch"
|
||||||
|
CATEGORY C1
|
||||||
|
TITLE "Geschichte"
|
||||||
|
USE_POOL HISTORY
|
||||||
|
LAYOUT GRID_2D
|
||||||
|
SIZE 6x6
|
||||||
|
PHASE NORMAL
|
||||||
|
QUESTION_SET NORMAL
|
||||||
|
CYCLES 3
|
||||||
|
TURNS_PER_CYCLE 4
|
||||||
|
TURN_ORDER ROTATING_START
|
||||||
|
PICK_MODE FREE_UNANSWERED
|
||||||
|
TEAM_MODE SOLO
|
||||||
|
SCORING SOLO_PLUS1_NO_PENALTY
|
||||||
|
PHASE BOSS
|
||||||
|
QUESTION_SET BOSS
|
||||||
|
TEAM_MODE TWO_VS_TWO
|
||||||
|
TEAM_ASSIGNMENT RANDOM
|
||||||
|
SCORING BOSS_2V2_PLUS2_PER_CORRECT_PLAYER
|
||||||
|
CATEGORY C2
|
||||||
|
TITLE "Filme"
|
||||||
|
USE_POOL MOVIES
|
||||||
|
LAYOUT GRID_2D
|
||||||
|
SIZE 6x6
|
||||||
|
PHASE NORMAL
|
||||||
|
QUESTION_SET NORMAL
|
||||||
|
CYCLES 3
|
||||||
|
TURNS_PER_CYCLE 4
|
||||||
|
TURN_ORDER ROTATING_START
|
||||||
|
PICK_MODE FREE_UNANSWERED
|
||||||
|
TEAM_MODE SOLO
|
||||||
|
SCORING SOLO_PLUS1_NO_PENALTY
|
||||||
|
PHASE BOSS
|
||||||
|
QUESTION_SET BOSS
|
||||||
|
TEAM_MODE TWO_VS_TWO
|
||||||
|
TEAM_ASSIGNMENT RANDOM
|
||||||
|
SCORING BOSS_2V2_PLUS2_PER_CORRECT_PLAYER
|
||||||
Reference in New Issue
Block a user