diff --git a/README.org b/README.org index 6f6186c8..9be0590c 100644 --- a/README.org +++ b/README.org @@ -32,7 +32,7 @@ Each letter represents a digit, and the goal is to reconstruct the original arit ** Prerequisites This project works on 32 bits and 64 bits environment and requires: - - JDK 11+ + - JDK 17+ - Maven 3+ - Cryptarithmetic Puzzle Solver in C (optional) - Download it from [[https://tamura70.gitlab.io/web-puzzle/cryptarithm/][Naoyuki Tamura Website]]. diff --git a/src/main/java/cryptator/Cryptator.java b/src/main/java/cryptator/Cryptator.java index ca7119d3..01cd7d58 100644 --- a/src/main/java/cryptator/Cryptator.java +++ b/src/main/java/cryptator/Cryptator.java @@ -109,7 +109,7 @@ private static long solve(final String cryptarithm, final CryptaParserWrapper pa final ICryptaNode node = parseCryptarithm(cryptarithm, parser, LOGGER); final CryptaBiConsumer consumer = buildBiConsumer(config); - final boolean solved = solver.solve(node, config, consumer); + final boolean solved = solver.solve(node, config, consumer).isFeasible(); String status = "ERROR"; if (consumer.getErrorCount() == 0) { status = solved ? "OK" : "KO"; diff --git a/src/main/java/cryptator/game/CryptaGameEngine.java b/src/main/java/cryptator/game/CryptaGameEngine.java index 1ef45cfa..d6ee294a 100644 --- a/src/main/java/cryptator/game/CryptaGameEngine.java +++ b/src/main/java/cryptator/game/CryptaGameEngine.java @@ -52,7 +52,7 @@ private boolean solveGame() { @Override public boolean isSolved() { - return userModel.getSolution().isTotalSolution(); + return userModel.getSolution().isTotalAssignment(); } private static CryptaModel makeUserDecisionModel(final CryptaModel model) { diff --git a/src/main/java/cryptator/gen/CryptaListGenerator.java b/src/main/java/cryptator/gen/CryptaListGenerator.java index 35201413..9876e2ab 100644 --- a/src/main/java/cryptator/gen/CryptaListGenerator.java +++ b/src/main/java/cryptator/gen/CryptaListGenerator.java @@ -35,6 +35,7 @@ import cryptator.specs.ICryptaNode; import cryptator.specs.ICryptaSolution; import cryptator.specs.ICryptaSolver; +import cryptator.specs.SearchMeasures; import cryptator.tree.TreeUtils; /** @@ -44,252 +45,256 @@ */ public class CryptaListGenerator implements ICryptaGenerator { - private static final int MIN_WORDS = 3; + private static final int MIN_WORDS = 3; - /** The words. */ - private final WordArray words; + /** The words. */ + private final WordArray words; - /** The config. */ - private final CryptagenConfig config; + /** The config. */ + private final CryptagenConfig config; - /** The logger. */ - private final Logger logger; + /** The logger. */ + private final Logger logger; - /** The clog. */ - private final ChocoLogger clog; + /** The clog. */ + private final ChocoLogger clog; - /** The error count. */ - private final AtomicInteger errorCount; + /** The error count. */ + private final AtomicInteger errorCount; - /** - * Instantiates a new cryptarithm generator. - * - * @param words the word array - * @param config the configuration - * @param logger the logger - */ - public CryptaListGenerator(final WordArray words, final CryptagenConfig config, final Logger logger) { - super(); - this.words = words; - this.config = config; - this.logger = logger; - this.clog = new ChocoLogger(logger); - this.errorCount = new AtomicInteger(); - } + /** + * Instantiates a new cryptarithm generator. + * + * @param words the word array + * @param config the configuration + * @param logger the logger + */ + public CryptaListGenerator(final WordArray words, final CryptagenConfig config, final Logger logger) { + super(); + this.words = words; + this.config = config; + this.logger = logger; + this.clog = new ChocoLogger(logger); + this.errorCount = new AtomicInteger(); + } - /** - * Gets the error count. - * - * @return the error count - */ - public final int getErrorCount() { - return errorCount.intValue(); - } + /** + * Gets the error count. + * + * @return the error count + */ + public final int getErrorCount() { + return errorCount.intValue(); + } - /** - * Creates the generation model with respect to the configuration. - * - * @return the generation model - */ - private AbstractCryptaListModel createGenModel() { - switch (config.getGenerateType()) { - case MUL: - return new CryptaGenMult(words.getWords(), config.getRightMemberType() != RightMemberType.FREE); - case LMUL: - return new CryptaGenLongMult(words.getWords(), config.getArithmeticBase()); - case CROSS: - return new CryptaGenCrossword(config.getGridSize(), words.getWords()); - default: - return new CryptaGenAdd(words.getWords(), config.getRightMemberType() != RightMemberType.FREE); - } - } + /** + * Creates the generation model with respect to the configuration. + * + * @return the generation model + */ + private AbstractCryptaListModel createGenModel() { + switch (config.getGenerateType()) { + case MUL: + return new CryptaGenMult(words.getWords(), config.getRightMemberType() != RightMemberType.FREE); + case LMUL: + return new CryptaGenLongMult(words.getWords(), config.getArithmeticBase()); + case CROSS: + return new CryptaGenCrossword(config.getGridSize(), words.getWords()); + default: + return new CryptaGenAdd(words.getWords(), config.getRightMemberType() != RightMemberType.FREE); + } + } - /** - * Builds the generation solver. - * - * It creates, builds and configures the generation model. - * - * @return the generation solver - */ - private ICryptaGenSolver buildGenSolver() { - final AbstractCryptaListModel gen = createGenModel(); - gen.buildModel(); - gen.postWordCountConstraints(Math.max(config.getMinWords(), MIN_WORDS), config.getMaxWords()); - gen.postMaxSymbolCountConstraint(config.getArithmeticBase()); - if (!config.isLightModel()) { - gen.postHeavyConstraints(config.getArithmeticBase()); - } - if (config.getRightMemberType() == RightMemberType.FIXED) { - gen.postFixedRightMemberConstraints(); - } - if (words.isDoublyTrue()) { - gen.postDoublyTrueConstraints(words.getLB()); - } - return gen; - } + /** + * Builds the generation solver. + * + * It creates, builds and configures the generation model. + * + * @return the generation solver + */ + private ICryptaGenSolver buildGenSolver() { + final AbstractCryptaListModel gen = createGenModel(); + gen.buildModel(); + gen.postWordCountConstraints(Math.max(config.getMinWords(), MIN_WORDS), config.getMaxWords()); + gen.postMaxSymbolCountConstraint(config.getArithmeticBase()); + if (!config.isLightModel()) { + gen.postHeavyConstraints(config.getArithmeticBase()); + } + if (config.getRightMemberType() == RightMemberType.FIXED) { + gen.postFixedRightMemberConstraints(); + } + if (words.isDoublyTrue()) { + gen.postDoublyTrueConstraints(words.getLB()); + } + return gen; + } - /** - * Builds the consumer for candidate cryptarithms . - * - * @param gen the generation model - * @param consumer the consumer for valid cryptarithms - * @return the candidate consumer - */ - private Consumer buildConsumer(final IChocoModel gen, - final BiConsumer consumer) { - final Consumer cons = new LogConsumer(gen); - final ICryptaSolver solver = Cryptator.createSolver(config); - return config.isDryRun() ? cons : cons.andThen(new GenerateConsumer(solver, consumer)); - } + /** + * Builds the consumer for candidate cryptarithms . + * + * @param gen the generation model + * @param consumer the consumer for valid cryptarithms + * @return the candidate consumer + */ + private Consumer buildConsumer(final IChocoModel gen, + final BiConsumer consumer) { + final Consumer cons = new LogConsumer(gen); + final ICryptaSolver solver = Cryptator.createSolver(config); + return config.isDryRun() ? cons : cons.andThen(new GenerateConsumer(solver, consumer)); + } - /** - * Solve the candidates in sequential. - * - * @param gen the generation solver - * @param cons the candidate consumer - */ - private void sequentialSolve(final ICryptaGenSolver gen, final Consumer cons) { - final Solver s = gen.getSolver(); - while (s.solve()) { - cons.accept(gen.recordCryptarithm()); - } - } + /** + * Solve the candidates in sequential. + * + * @param gen the generation solver + * @param cons the candidate consumer + */ + private void sequentialSolve(final ICryptaGenSolver gen, final Consumer cons) { + final Solver s = gen.getSolver(); + while (s.solve()) { + cons.accept(gen.recordCryptarithm()); + } + } - /** - * Solve the candidates in parallel (experimental). - * - * @param gen the generation solver - * @param cons the candidate consumer - * @param nthreads the number of threads - */ - private static void parallelSolve(final ICryptaGenSolver gen, final Consumer cons, - final int nthreads) { - final ExecutorService executor = Executors.newFixedThreadPool(nthreads); - final Solver s = gen.getSolver(); - while (s.solve()) { - final ICryptaNode cryptarithm = gen.recordCryptarithm(); - executor.execute(() -> cons.accept(cryptarithm)); - } - try { - if (!executor.awaitTermination(10000, TimeUnit.MILLISECONDS)) { - executor.shutdownNow(); - } - } catch (InterruptedException e) { - executor.shutdownNow(); - Thread.currentThread().interrupt(); - } - } + /** + * Solve the candidates in parallel (experimental). + * + * @param gen the generation solver + * @param cons the candidate consumer + * @param nthreads the number of threads + */ + private static void parallelSolve(final ICryptaGenSolver gen, final Consumer cons, + final int nthreads) { + final ExecutorService executor = Executors.newFixedThreadPool(nthreads); + try { + final Solver s = gen.getSolver(); + while (s.solve()) { + final ICryptaNode cryptarithm = gen.recordCryptarithm(); + executor.execute(() -> cons.accept(cryptarithm)); + } + } finally { + executor.shutdown(); // first signal shutdown + try { + if (!executor.awaitTermination(10, TimeUnit.SECONDS)) { + executor.shutdownNow(); + } + } catch (InterruptedException e) { + executor.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + } - @Override - public long generate(final BiConsumer consumer) throws CryptaModelException { - final ICryptaGenSolver gen = buildGenSolver(); - clog.logOnModel(gen); + @Override + public SearchMeasures generate(final BiConsumer consumer) throws CryptaModelException { + final ICryptaGenSolver gen = buildGenSolver(); + clog.logOnModel(gen); - final Consumer cons = buildConsumer(gen, consumer); + final Consumer cons = buildConsumer(gen, consumer); - final int nthreads = config.getNthreads(); - if (nthreads == 1) { - sequentialSolve(gen, cons); - } else { - parallelSolve(gen, cons, nthreads); - } - clog.logOnSolver(gen); - return gen.getSolver().getSolutionCount(); - } + final int nthreads = config.getNthreads(); + if (nthreads == 1) { + sequentialSolve(gen, cons); + } else { + parallelSolve(gen, cons, nthreads); + } + clog.logOnSolver(gen); + return new SearchMeasures(gen.getSolver()); + } - /** - * The LogConsumer logs the candidate cryptarithm. - */ - // FIXME are consumers thread-safe ? they are used in parallel ! - private class LogConsumer implements Consumer { + /** + * The LogConsumer logs the candidate cryptarithm. + */ + // FIXME are consumers thread-safe ? they are used in parallel ! + private class LogConsumer implements Consumer { - /** The solution. */ - private final Solution solution; + /** The solution. */ + private final Solution solution; - /** - * Instantiates a new log consumer. - * - * @param gen the generation model - */ - LogConsumer(final IChocoModel gen) { - super(); - solution = new Solution(gen.getModel()); - } + /** + * Instantiates a new log consumer. + * + * @param gen the generation model + */ + LogConsumer(final IChocoModel gen) { + super(); + solution = new Solution(gen.getModel()); + } - /** - * Accept the candidate cryptarithm. - * - * @param t the candidate cryptarithm - */ - @Override - public void accept(final ICryptaNode t) { - clog.logOnSolution(solution); - if (logger.isLoggable(Level.FINE)) { - logger.log(Level.FINE, "Candidate cryptarithm:\n{0}", TreeUtils.writeInorder(t)); - } - } - } + /** + * Accept the candidate cryptarithm. + * + * @param t the candidate cryptarithm + */ + @Override + public void accept(final ICryptaNode t) { + clog.logOnSolution(solution); + if (logger.isLoggable(Level.FINE)) { + logger.log(Level.FINE, "Candidate cryptarithm:\n{0}", TreeUtils.writeInorder(t)); + } + } + } - /** - * The Class GenerateConsumer solves the candidate cryptarithm. - */ - private class GenerateConsumer implements Consumer { + /** + * The Class GenerateConsumer solves the candidate cryptarithm. + */ + private class GenerateConsumer implements Consumer { - /** The solver. */ - private final ICryptaSolver solver; + /** The solver. */ + private final ICryptaSolver solver; - /** The internal consumer for valid cryptarithm. */ - private final BiConsumer internal; + /** The internal consumer for valid cryptarithm. */ + private final BiConsumer internal; - /** - * Instantiates a new generate consumer. - * - * @param solver the cryptarithm solver - * @param internal the internal consumer for valid cryptarithm - */ - GenerateConsumer(final ICryptaSolver solver, final BiConsumer internal) { - super(); - this.solver = solver; - this.internal = internal; - this.solver.limitSolution(2); - } + /** + * Instantiates a new generate consumer. + * + * @param solver the cryptarithm solver + * @param internal the internal consumer for valid cryptarithm + */ + GenerateConsumer(final ICryptaSolver solver, final BiConsumer internal) { + super(); + this.solver = solver; + this.internal = internal; + this.solver.limitSolution(2); + } - /** - * Builds the consumer of valid cryptarithms. - * - * @return the cryptarithm consumer - */ - private CryptaBiConsumer buildBiConsumer() { - CryptaBiConsumer consumer = new CryptaBiConsumer(logger); - if (config.isCheckSolution()) { - consumer.withSolutionCheck(config.getArithmeticBase()); - } - return consumer; - } + /** + * Builds the consumer of valid cryptarithms. + * + * @return the cryptarithm consumer + */ + private CryptaBiConsumer buildBiConsumer() { + CryptaBiConsumer consumer = new CryptaBiConsumer(logger); + if (config.isCheckSolution()) { + consumer.withSolutionCheck(config.getArithmeticBase()); + } + return consumer; + } - /** - * Accept the candidate cryptarithm. - * - * @param t the candidate - */ - @Override - public void accept(final ICryptaNode t) { - try { - final CryptaBiConsumer collect = buildBiConsumer(); - solver.solve(t, config, collect); - if (collect.getErrorCount() == 0) { - final Optional solution = collect.getUniqueSolution(); - if (solution.isPresent()) { - internal.accept(t, solution.get()); - } - } else { - logger.log(Level.WARNING, "Solve the candidate cryptarithm [ERROR]"); - } - } catch (CryptaModelException | CryptaSolverException e) { - errorCount.incrementAndGet(); - logger.log(Level.WARNING, "Solve the candidate cryptarithm [FAIL]", e); - } - } - } + /** + * Accept the candidate cryptarithm. + * + * @param t the candidate + */ + @Override + public void accept(final ICryptaNode t) { + try { + final CryptaBiConsumer collect = buildBiConsumer(); + solver.solve(t, config, collect); + if (collect.getErrorCount() == 0) { + final Optional solution = collect.getUniqueSolution(); + if (solution.isPresent()) { + internal.accept(t, solution.get()); + } + } else { + logger.log(Level.WARNING, "Solve the candidate cryptarithm [ERROR]"); + } + } catch (CryptaModelException | CryptaSolverException e) { + errorCount.incrementAndGet(); + logger.log(Level.WARNING, "Solve the candidate cryptarithm [FAIL]", e); + } + } + } } diff --git a/src/main/java/cryptator/solver/AdaptiveSolver.java b/src/main/java/cryptator/solver/AdaptiveSolver.java index 31636921..2f416211 100644 --- a/src/main/java/cryptator/solver/AdaptiveSolver.java +++ b/src/main/java/cryptator/solver/AdaptiveSolver.java @@ -20,6 +20,7 @@ import cryptator.specs.ICryptaSolution; import cryptator.specs.ICryptaSolver; import cryptator.specs.ITraversalNodeConsumer; +import cryptator.specs.SearchMeasures; import cryptator.tree.TreeTraversals; public class AdaptiveSolver implements ICryptaSolver { @@ -56,7 +57,7 @@ public void limitSolution(final long limit) { } @Override - public boolean solve(final ICryptaNode cryptarithm, final CryptaConfig config, + public SearchMeasures solve(final ICryptaNode cryptarithm, final CryptaConfig config, final Consumer solutionConsumer) throws CryptaModelException, CryptaSolverException { final AdaptiveConsumer cons = new AdaptiveConsumer(); TreeTraversals.preorderTraversal(cryptarithm, cons); diff --git a/src/main/java/cryptator/solver/CryptaSolutionMap.java b/src/main/java/cryptator/solver/CryptaSolutionMap.java index a770615f..11d95aae 100644 --- a/src/main/java/cryptator/solver/CryptaSolutionMap.java +++ b/src/main/java/cryptator/solver/CryptaSolutionMap.java @@ -15,11 +15,12 @@ public class CryptaSolutionMap extends AbstractCryptaSolution { - public CryptaSolutionMap(final Map symbolsToDigits) { - super(symbolsToDigits); - } + + public CryptaSolutionMap(Map symbolsToDigits) { + super(symbolsToDigits); + } - public static final ICryptaSolution parseSolution(final String solution) throws CryptaSolutionException { + public static final ICryptaSolution parseSolution(final String solution) throws CryptaSolutionException { final HashMap symbolToDigit = new HashMap<>(); final String[] split = solution.split("\\s*[\\s=]\\s*"); if ((split.length % 2) != 0) { @@ -59,10 +60,17 @@ public int getDigit(final char symbol) throws CryptaSolutionException { public int getDigit(final char symbol, final int defaultValue) { return symbolsToDigits.getOrDefault(symbol, defaultValue); } + + @Override + public Map toMap() { + return new HashMap<>(symbolsToDigits); + } - @Override + @Override protected String getDomain(final Integer v) { return v.toString(); } + + } diff --git a/src/main/java/cryptator/solver/CryptaSolutionVars.java b/src/main/java/cryptator/solver/CryptaSolutionVars.java index 36d1dc0d..3d518464 100644 --- a/src/main/java/cryptator/solver/CryptaSolutionVars.java +++ b/src/main/java/cryptator/solver/CryptaSolutionVars.java @@ -8,67 +8,67 @@ */ package cryptator.solver; -import java.util.HashMap; import java.util.Map; +import java.util.stream.Collectors; import org.chocosolver.solver.variables.IntVar; +import org.chocosolver.solver.variables.Variable; import cryptator.specs.ICryptaSolution; public class CryptaSolutionVars extends AbstractCryptaSolution { - public CryptaSolutionVars(final Map symbolsToDigits) { - super(symbolsToDigits); - } + public CryptaSolutionVars(final Map symbolsToDigits) { + super(symbolsToDigits); + } - @Override - public boolean hasDigit(final char symbol) { - final IntVar v = symbolsToDigits.get(symbol); - return (v != null) && v.isInstantiated(); - } + @Override + public boolean hasDigit(final char symbol) { + final IntVar v = symbolsToDigits.get(symbol); + return (v != null) && v.isInstantiated(); + } - @Override - public int getDigit(final char symbol) throws CryptaSolutionException { - final IntVar v = symbolsToDigits.get(symbol); - if ((v != null) && v.isInstantiated()) { - return v.getValue(); - } else { - throw new CryptaSolutionException("cant find symbol: " + symbol); - } - } + @Override + public int getDigit(final char symbol) throws CryptaSolutionException { + final IntVar v = symbolsToDigits.get(symbol); + if ((v != null) && v.isInstantiated()) { + return v.getValue(); + } else { + throw new CryptaSolutionException("cant find symbol: " + symbol); + } + } - @Override - public int getDigit(final char symbol, final int defaultValue) { - final IntVar v = symbolsToDigits.get(symbol); - if ((v != null) && v.isInstantiated()) { - return v.getValue(); - } else { - return defaultValue; - } - } + @Override + public int getDigit(final char symbol, final int defaultValue) { + final IntVar v = symbolsToDigits.get(symbol); + if ((v != null) && v.isInstantiated()) { + return v.getValue(); + } else { + return defaultValue; + } + } - @Override - protected final String getDomain(final IntVar v) { - return v.toString().replaceFirst(".*=\\s*", ""); - } + @Override + protected final String getDomain(final IntVar v) { + return v.toString().replaceFirst(".*=\\s*", ""); + } - public ICryptaSolution recordSolution() { - final Map symbolsToDigits = new HashMap<>(); - this.symbolsToDigits.forEach((symbol, variable) -> { - if (variable.isInstantiated()) { - symbolsToDigits.put(symbol, variable.getValue()); - } - }); - return new CryptaSolutionMap(symbolsToDigits); - } + public ICryptaSolution recordSolution() { + return new CryptaSolutionMap(toMap()); + } - public boolean isTotalSolution() { - for (IntVar variable : symbolsToDigits.values()) { - if (!variable.isInstantiated()) { - return false; - } - } - return true; - } + @Override + public Map toMap() { + return symbolsToDigits.entrySet().stream() + .filter(entry -> entry.getValue().isInstantiated()) + .collect(Collectors.toMap( + Map.Entry::getKey, + entry -> entry.getValue().getValue() + )); + } + + public boolean isTotalAssignment() { + return symbolsToDigits.values().stream().allMatch(Variable::isInstantiated); + } } diff --git a/src/main/java/cryptator/solver/CryptaSolver.java b/src/main/java/cryptator/solver/CryptaSolver.java index 579a47fd..ef6fd929 100644 --- a/src/main/java/cryptator/solver/CryptaSolver.java +++ b/src/main/java/cryptator/solver/CryptaSolver.java @@ -17,6 +17,7 @@ import cryptator.specs.ICryptaModeler; import cryptator.specs.ICryptaNode; import cryptator.specs.ICryptaSolution; +import cryptator.specs.SearchMeasures; public final class CryptaSolver extends AbstractCryptaSolver { @@ -42,8 +43,8 @@ public void unsetBignum() { } @Override - public boolean solve(final ICryptaNode cryptarithm, final CryptaConfig config, - final Consumer solutionConsumer) throws CryptaModelException { + public SearchMeasures solve(final ICryptaNode cryptarithm, final CryptaConfig config, + final Consumer solutionConsumer) throws CryptaModelException, CryptaSolverException { final CryptaModel m = modeler.model(cryptarithm, config); logOnCryptarithm(cryptarithm); logOnConfiguration(config); @@ -53,7 +54,8 @@ public boolean solve(final ICryptaNode cryptarithm, final CryptaConfig config, if (timeLimit > 0) { s.limitTime(timeLimit * MS); // in ms } - int solutionCount = 0; + + long solutionCount = 0; if (solutionLimit > 0) { while ((solutionCount < solutionLimit) && s.solve()) { CLOG.logOnSolution(m.getModel()); @@ -68,7 +70,10 @@ public boolean solve(final ICryptaNode cryptarithm, final CryptaConfig config, } } CLOG.logOnSolver(m); - return solutionCount > 0; + if(solutionCount != s.getSolutionCount()) { + throw new CryptaSolverException("Inconsistent solution count"); + } + return new SearchMeasures(s); } } diff --git a/src/main/java/cryptator/solver/crypt/CryptSolver.java b/src/main/java/cryptator/solver/crypt/CryptSolver.java index 19ff014c..db8247f9 100644 --- a/src/main/java/cryptator/solver/crypt/CryptSolver.java +++ b/src/main/java/cryptator/solver/crypt/CryptSolver.java @@ -22,6 +22,7 @@ import cryptator.solver.CryptaSolverException; import cryptator.specs.ICryptaNode; import cryptator.specs.ICryptaSolution; +import cryptator.specs.SearchMeasures; import cryptator.tree.TreeUtils; /** @@ -30,7 +31,7 @@ public class CryptSolver extends AbstractCryptaSolver { @Override - public boolean solve(final ICryptaNode cryptarithm, final CryptaConfig config, + public SearchMeasures solve(final ICryptaNode cryptarithm, final CryptaConfig config, final Consumer consumer) throws CryptaModelException, CryptaSolverException { logOnCryptarithm(cryptarithm); logOnConfiguration(config); @@ -45,17 +46,20 @@ public boolean solve(final ICryptaNode cryptarithm, final CryptaConfig config, b.append(TreeUtils.writeInorder(cryptarithm)).append("\n"); // Solve the cryptarithm with the crypt solver try { + + long timeCount = -System.nanoTime(); final CryptExec crypt = new CryptExec(((CryptaCmdConfig) config).getCryptCommand()); + timeCount += System.nanoTime(); final CryptConsumer cryptConsumer = new CryptConsumer(consumer); crypt.exec(b.toString().getBytes(), cryptConsumer); - return cryptConsumer.getSolutionCount() > 0; + return new SearchMeasures(cryptConsumer.getSolutionCount(), timeCount, 0, 0); } catch (IOException e) { LOGGER.log(Level.SEVERE, "Crypt solver error", e); } catch (InterruptedException e) { LOGGER.log(Level.SEVERE, "Crypt solver interruption", e); Thread.currentThread().interrupt(); } - return false; + return new SearchMeasures(0, 0, 0, 1); } /** diff --git a/src/main/java/cryptator/specs/ICryptaGenerator.java b/src/main/java/cryptator/specs/ICryptaGenerator.java index 6975edd8..5e39ef75 100644 --- a/src/main/java/cryptator/specs/ICryptaGenerator.java +++ b/src/main/java/cryptator/specs/ICryptaGenerator.java @@ -23,9 +23,9 @@ public interface ICryptaGenerator { * * @param consumer the consumer that handles the generated cryptarithm along * with its solution. - * @return the number of candidate cryptarithm + * @return the search measures * @throws CryptaModelException if there was an error during the generation. */ - long generate(BiConsumer consumer) throws CryptaModelException; + SearchMeasures generate(BiConsumer consumer) throws CryptaModelException; } diff --git a/src/main/java/cryptator/specs/ICryptaSolution.java b/src/main/java/cryptator/specs/ICryptaSolution.java index 7d5f7400..82c55897 100644 --- a/src/main/java/cryptator/specs/ICryptaSolution.java +++ b/src/main/java/cryptator/specs/ICryptaSolution.java @@ -8,6 +8,8 @@ */ package cryptator.specs; +import java.util.Map; + import cryptator.solver.CryptaSolutionException; /** @@ -65,5 +67,15 @@ public interface ICryptaSolution { * @return the domain no matter the symbol appears in the cryptarithm. */ String getDomain(char symbol); + + + /** + * Returns the current assignment of symbols to digits. + * Unassigned symbols are absent. + * + * @return a map from symbols to their assigned digits. + */ + Map toMap(); + } diff --git a/src/main/java/cryptator/specs/ICryptaSolver.java b/src/main/java/cryptator/specs/ICryptaSolver.java index 8d4cda5b..54a8f1da 100644 --- a/src/main/java/cryptator/specs/ICryptaSolver.java +++ b/src/main/java/cryptator/specs/ICryptaSolver.java @@ -40,11 +40,11 @@ public interface ICryptaSolver { * @param cryptarithm the cryptarithm * @param config the configuration * @param consumer the solution consumer - * @return true, if it is feasible. + * @return the search measures * @throws CryptaModelException if there is a modeling exception * @throws CryptaSolverException if there is a solving exception. */ - boolean solve(ICryptaNode cryptarithm, CryptaConfig config, Consumer consumer) + SearchMeasures solve(ICryptaNode cryptarithm, CryptaConfig config, Consumer consumer) throws CryptaModelException, CryptaSolverException; /** @@ -58,7 +58,7 @@ boolean solve(ICryptaNode cryptarithm, CryptaConfig config, Consumer consumer) throws CryptaModelException, CryptaSolverException { return solve(cryptarithm, config, solution -> consumer.accept(cryptarithm, solution)); diff --git a/src/main/java/cryptator/specs/SearchMeasures.java b/src/main/java/cryptator/specs/SearchMeasures.java new file mode 100644 index 00000000..5d7670b9 --- /dev/null +++ b/src/main/java/cryptator/specs/SearchMeasures.java @@ -0,0 +1,39 @@ +/* + * This file is part of cryptator, https://github.com/arnaud-m/cryptator + * + * Copyright (c) 2021-2026, Université Côte d'Azur. All rights reserved. + * + * Licensed under the BSD 3-clause license. + * See LICENSE file in the project root for full license information. + */ +package cryptator.specs; + +import org.chocosolver.solver.Solver; + +/** + * Immutable container for search performance measures. + * + * @param solutionCount number of solutions found + * @param timeCount execution time of the search in seconds + * @param nodeCount number of explored nodes + * @param errorCount number of errors + */ +public record SearchMeasures(long solutionCount, double timeCount, long nodeCount, int errorCount) { + + public SearchMeasures(Solver solver) { + this(solver.getSolutionCount(), solver.getTimeCount(), solver.getSolutionCount(), 0); + } + + public final SearchMeasures withErrorCount(int errorCount) { + return new SearchMeasures(this.solutionCount, this.timeCount, this.nodeCount, errorCount); + } + + /** + * Indicates whether at least one solution was found. + * + * @return {@code true} if at least one solution exists + */ + public final boolean isFeasible() { + return solutionCount > 0; + } +} \ No newline at end of file diff --git a/src/test/java/cryptator/GenerateTest.java b/src/test/java/cryptator/GenerateTest.java index c8bc68ce..3d06574b 100644 --- a/src/test/java/cryptator/GenerateTest.java +++ b/src/test/java/cryptator/GenerateTest.java @@ -53,7 +53,7 @@ private void testGenerate(final int expectedSolCount, final OptionalInt expected cons.withSolutionLog(); cons.withSolutionCheck(config.getArithmeticBase()); assertEquals(0, cons.getErrorCount()); - long actualCandCount = gen.generate(cons); + final long actualCandCount = gen.generate(cons).solutionCount(); if (expectedCandCount.isPresent()) { assertEquals(expectedCandCount.getAsInt(), actualCandCount); } diff --git a/src/test/java/cryptator/SolverTest.java b/src/test/java/cryptator/SolverTest.java index 5e62127f..a392bba7 100644 --- a/src/test/java/cryptator/SolverTest.java +++ b/src/test/java/cryptator/SolverTest.java @@ -70,7 +70,7 @@ public int testSolve(final String cryptarithm, final boolean hasSolution) e.printStackTrace(); fail(); } - })); + }).isFeasible()); return solutionCount.get(); }