mtg-genetic-deckbuilding

Generating and improving Magic: The Gathering decks using a genetic algorithm
git clone https://kevincorvisier.fr/git/mtg-genetic-deckbuilding.git
Log | Files | Refs | LICENSE

commit c27dec529b18df27ba8907059168e7b26ab00f8f
parent 2fe7dd0f837bdb8c90d4532231ed7e5e1fc80069
Author: Kevin Corvisier <git@kevincorvisier.fr>
Date:   Sun, 22 Dec 2024 22:37:04 +0900

Rework validation service
Diffstat:
Msrc/main/java/fr/kevincorvisier/mtg/gdb/evaluation/WinRatioEvaluation.java | 23++++++++++++++++++-----
Msrc/main/java/fr/kevincorvisier/mtg/gdb/population/NextGenerationService.java | 59++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Msrc/main/java/fr/kevincorvisier/mtg/gdb/population/PopulationFactory.java | 7+++----
Msrc/main/java/fr/kevincorvisier/mtg/gdb/utils/CardListFile.java | 5+++++
Asrc/main/java/fr/kevincorvisier/mtg/gdb/validation/CardValidator.java | 10++++++++++
Msrc/main/java/fr/kevincorvisier/mtg/gdb/validation/ErrorProneCardsService.java | 9++++++++-
Dsrc/main/java/fr/kevincorvisier/mtg/gdb/validation/IndividualValidationService.java | 169-------------------------------------------------------------------------------
Asrc/main/java/fr/kevincorvisier/mtg/gdb/validation/ValidationService.java | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/main/java/fr/kevincorvisier/mtg/gdb/validation/individual/CardPoolIndividualValidator.java | 27+++++++++++++++++++++++++++
Asrc/main/java/fr/kevincorvisier/mtg/gdb/validation/individual/ConditionsIndividualValidator.java | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/main/java/fr/kevincorvisier/mtg/gdb/validation/individual/FormatIndividualValidator.java | 32++++++++++++++++++++++++++++++++
Asrc/main/java/fr/kevincorvisier/mtg/gdb/validation/individual/IndividualValidator.java | 12++++++++++++
Asrc/main/java/fr/kevincorvisier/mtg/gdb/validation/individual/MaxUniqueCardsIndividualValidator.java | 29+++++++++++++++++++++++++++++
13 files changed, 347 insertions(+), 184 deletions(-)

diff --git a/src/main/java/fr/kevincorvisier/mtg/gdb/evaluation/WinRatioEvaluation.java b/src/main/java/fr/kevincorvisier/mtg/gdb/evaluation/WinRatioEvaluation.java @@ -5,6 +5,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.List; import javax.validation.constraints.NotNull; @@ -22,6 +23,7 @@ import fr.kevincorvisier.mtg.gdb.population.Individual; import fr.kevincorvisier.mtg.gdb.population.Population; import fr.kevincorvisier.mtg.gdb.utils.MagicOnlineDeckLoader; import fr.kevincorvisier.mtg.gdb.validation.ErrorProneCardsService; +import fr.kevincorvisier.mtg.gdb.validation.ValidationService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -35,6 +37,7 @@ public class WinRatioEvaluation implements Evaluation private final MagicOnlineDeckLoader forgeUtils; private final ErrorProneCardsService errorProneCards; + private final ValidationService validationService; @Value("${evaluation.win-ratio.opponents-directories}") private final File[] opponentsDirectories; @@ -76,11 +79,21 @@ public class WinRatioEvaluation implements Evaluation public void evaluateFitness(final Population population) { population.parallelStream() // - .forEach(individual -> calculationWinRatio(individual, opponents)); + .forEach(this::calculationWinRatio); } - private void calculationWinRatio(final Individual individual, final Collection<Deck> opponents) + private void calculationWinRatio(final Individual individual) { + // Revalidate in case validation rules changes (error-prone cards) + final String conformationProblem = validationService.validate(individual); + if (conformationProblem != null) + { + log.warn("calculationWinRatio: deck {} became invalid: {}", individual.getName(), conformationProblem); + return; + } + + final List<Deck> usedOpponents = opponents.stream().filter(deck -> validationService.validateDeck(deck) == null).toList(); + WinRatioEvaluationContext context = individual.getWinRatioContext(); if (context == null) { @@ -89,19 +102,19 @@ public class WinRatioEvaluation implements Evaluation } final int minGames = context.getGamesPlayed() == 0 ? initialMinGames : subsequentMinGames; - final int gamesPerOpponent = (int) Math.ceil((double) (minGames) / (double) (opponents.size())); + final int gamesPerOpponent = (int) Math.ceil((double) (minGames) / (double) (usedOpponents.size())); final GameRules rules = new GameRules(GameType.Constructed); rules.setGamesPerMatch(1); - log.debug("individual={}, minGames={}, opponents={}, gamesPerMatch={}", individual.getName(), minGames, opponents.size(), gamesPerOpponent); + log.debug("individual={}, minGames={}, opponents={}, gamesPerMatch={}", individual.getName(), minGames, usedOpponents.size(), gamesPerOpponent); for (int i = 0; i != gamesPerOpponent; i++) { if (skipEvaluation(individual, context)) return; - opponents.parallelStream().forEach(opponentDeck -> { + usedOpponents.parallelStream().forEach(opponentDeck -> { final RegisteredPlayer player = new RegisteredPlayer(individual.getDeck()) .setPlayer(GamePlayerUtil.createAiPlayer(individual.getName(), playerAiProfile)); final RegisteredPlayer opponent = new RegisteredPlayer(opponentDeck) diff --git a/src/main/java/fr/kevincorvisier/mtg/gdb/population/NextGenerationService.java b/src/main/java/fr/kevincorvisier/mtg/gdb/population/NextGenerationService.java @@ -14,12 +14,14 @@ import fr.kevincorvisier.mtg.gdb.operators.mutation.Mutation; import fr.kevincorvisier.mtg.gdb.operators.selection.HybridSelection; import fr.kevincorvisier.mtg.gdb.operators.selection.Selection; import fr.kevincorvisier.mtg.gdb.utils.RandomDeckGenerator; -import fr.kevincorvisier.mtg.gdb.validation.IndividualValidationService; +import fr.kevincorvisier.mtg.gdb.validation.ValidationService; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; /** * From a population (after evaluation), create a new population for the next generation */ +@Slf4j @Service @RequiredArgsConstructor public class NextGenerationService @@ -35,12 +37,19 @@ public class NextGenerationService @Value("${mutation.rate}") private final double mutationRate; + /** + * Avoid being in an infinite loop if no new decks can be validated + */ + private long consecutiveFailures = 0; + + private final Collection<Long> alreadyGenerated = new HashSet<>(); + private final Crossover crossover; private final Mutation mutation; private final Selection selection = new HybridSelection(); private final RandomDeckGenerator deckGenerator; - private final IndividualValidationService validationService; + private final ValidationService validationService; private final Random rnd = new Random(); @@ -61,7 +70,12 @@ public class NextGenerationService final int eliteCount = Math.min(population.getSize(), Math.round(maximumSize * elitismRate)); for (int i = 0; i != eliteCount; i++) - next.addIndividual(population.get(i)); + { + final Individual child = population.get(i); + + if (validate(child, next)) + next.addIndividual(child); + } } private void addNewShare(final DefaultPopulation next) @@ -74,7 +88,7 @@ public class NextGenerationService final Deck deck = deckGenerator.getRandomDeck(); final Individual child = new Individual("g0_new_" + next.getSize(), deck); - if (validationService.validate(child, next)) + if (validate(child, next)) next.addIndividual(child); } } @@ -107,9 +121,44 @@ public class NextGenerationService if (result.size() >= maximumSize) continue; // Ensure second child will not breach the population limit - if (validationService.validate(individual, next)) + if (validate(individual, next)) next.addIndividual(individual); } } } + + /* package */ boolean validate(final Individual child, final Population population) + { + if (consecutiveFailures == maximumSize * 100) + { + log.error("validate: loop soft limit reached, reseting alreadyGenerated"); + alreadyGenerated.clear(); + population.stream().forEach(i -> alreadyGenerated.add(i.getFingerprint())); + } + else if (consecutiveFailures == maximumSize * 1000) + { + log.error("validate: loop hard limit reached, exiting"); + System.exit(0); + } + + final String conformanceProblem = validationService.validate(child); + if (conformanceProblem != null) + { + log.warn("Ignoring child: {}", conformanceProblem); + consecutiveFailures++; + return false; + } + + final long fingerprint = child.getFingerprint(); + if (alreadyGenerated.contains(fingerprint)) + { + log.warn("Ignoring child: already generated"); + consecutiveFailures++; + return false; + } + + consecutiveFailures = 0; + alreadyGenerated.add(fingerprint); + return true; + } } diff --git a/src/main/java/fr/kevincorvisier/mtg/gdb/population/PopulationFactory.java b/src/main/java/fr/kevincorvisier/mtg/gdb/population/PopulationFactory.java @@ -8,7 +8,6 @@ import org.springframework.stereotype.Service; import forge.deck.Deck; import fr.kevincorvisier.mtg.gdb.utils.MagicOnlineDeckLoader; import fr.kevincorvisier.mtg.gdb.utils.RandomDeckGenerator; -import fr.kevincorvisier.mtg.gdb.validation.IndividualValidationService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -19,7 +18,7 @@ public class PopulationFactory { private final MagicOnlineDeckLoader forgeUtils; private final RandomDeckGenerator deckGenerator; - private final IndividualValidationService validationService; + private final NextGenerationService nextGenerationService; @Value("${population.initialization.disk.directories}") private final File[] directories; @@ -62,7 +61,7 @@ public class PopulationFactory for (final Deck deck : forgeUtils.loadDecks(dir)) { final Individual individual = new Individual(deck.getName(), deck); - if (validationService.validate(individual, population)) + if (nextGenerationService.validate(individual, population)) population.addIndividual(individual); else log.warn("Deck from initial population failed validation: ", deck.getName(), deck); @@ -79,7 +78,7 @@ public class PopulationFactory while (population.getSize() < maximumSize) { final Individual individual = new Individual("g0_" + population.getSize(), deckGenerator.getRandomDeck()); - if (validationService.validate(individual, population)) + if (nextGenerationService.validate(individual, population)) population.addIndividual(individual); } diff --git a/src/main/java/fr/kevincorvisier/mtg/gdb/utils/CardListFile.java b/src/main/java/fr/kevincorvisier/mtg/gdb/utils/CardListFile.java @@ -40,6 +40,11 @@ public class CardListFile save(); } + public boolean contains(final String name) + { + return list.contains(name); + } + private void load() throws IOException { if (!file.exists() && list == null && !loaded) diff --git a/src/main/java/fr/kevincorvisier/mtg/gdb/validation/CardValidator.java b/src/main/java/fr/kevincorvisier/mtg/gdb/validation/CardValidator.java @@ -0,0 +1,10 @@ +package fr.kevincorvisier.mtg.gdb.validation; + +import javax.validation.constraints.NotNull; + +import forge.item.PaperCard; + +public interface CardValidator +{ + boolean isValid(@NotNull final PaperCard card); +} diff --git a/src/main/java/fr/kevincorvisier/mtg/gdb/validation/ErrorProneCardsService.java b/src/main/java/fr/kevincorvisier/mtg/gdb/validation/ErrorProneCardsService.java @@ -9,6 +9,7 @@ import javax.validation.constraints.NotNull; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import forge.item.PaperCard; import fr.kevincorvisier.mtg.gdb.utils.CardListFile; import lombok.NonNull; import lombok.RequiredArgsConstructor; @@ -17,7 +18,7 @@ import lombok.extern.slf4j.Slf4j; @Slf4j @Service @RequiredArgsConstructor -public class ErrorProneCardsService +public class ErrorProneCardsService implements CardValidator { private static final Pattern PATTERN_INVALID_PARAMETER_EXCEPTION = Pattern .compile("^(?<name>.+) \\(\\d+\\)'s ability resulted in no types to choose from$"); @@ -40,4 +41,10 @@ public class ErrorProneCardsService else log.warn("handleException uable to handle {}", e.getClass().getName()); } + + @Override + public boolean isValid(@NonNull @NotNull final PaperCard card) + { + return cards.contains(card.getName()); + } } diff --git a/src/main/java/fr/kevincorvisier/mtg/gdb/validation/IndividualValidationService.java b/src/main/java/fr/kevincorvisier/mtg/gdb/validation/IndividualValidationService.java @@ -1,169 +0,0 @@ -package fr.kevincorvisier.mtg.gdb.validation; - -import java.util.Collection; -import java.util.HashSet; -import java.util.stream.Collectors; - -import javax.annotation.Nullable; - -import org.apache.commons.lang3.StringUtils; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; - -import forge.deck.Deck; -import forge.deck.Deck.UnplayableAICards; -import forge.deck.DeckFormat; -import forge.deck.DeckSection; -import forge.game.GameFormat; -import forge.item.PaperCard; -import fr.kevincorvisier.mtg.gdb.ai.CardPoolService; -import fr.kevincorvisier.mtg.gdb.population.Individual; -import fr.kevincorvisier.mtg.gdb.population.Population; -import lombok.Data; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -@Slf4j -@Service -@RequiredArgsConstructor -public class IndividualValidationService -{ - private static final DeckFormat DECK_FORMAT = DeckFormat.Constructed; - - private final Collection<Condition> conditions = new HashSet<>(); - @Value("${validation.child.max-unique-cards}") - private final int maxUniqueCards; - - @Value("${population.maximum-size}") - private final int maximumSize; - - private final Collection<Long> alreadyGenerated = new HashSet<>(); - - private final GameFormat format; - private final CardPoolService cardPool; - - /** - * Avoid being in an infinite loop if no new decks can be validated - */ - private long consecutiveFailures = 0; - - @Value("${validation.child.conditions}") - public void setConditions(final String conditions) - { - if (conditions != null) - { - for (final String condition : StringUtils.split(conditions, ';')) - { - final String[] words = StringUtils.split(condition.trim(), ' '); - - if (words.length < 3) - throw new RuntimeException("Invalid validation condition: " + condition); - - final String cardName = StringUtils.join(words, ' ', 0, words.length - 2); - final String op = words[words.length - 2]; - final int count = Integer.parseInt(words[words.length - 1]); - - if (!StringUtils.equalsAny(op, "=", "<=")) - throw new RuntimeException("Invalid validation condition: " + condition); - - this.conditions.add(new Condition(cardName, op, count)); - } - } - } - - public boolean validate(final Individual child, final Population population) - { - if (consecutiveFailures == maximumSize * 100) - { - log.error("toNextGeneration: loop soft limit reached, reseting alreadyGenerated"); - alreadyGenerated.clear(); - population.stream().forEach(i -> alreadyGenerated.add(i.getFingerprint())); - } - else if (consecutiveFailures == maximumSize * 1000) - { - log.error("toNextGeneration: loop hard limit reached, exiting"); - System.exit(0); - } - - final String conformanceProblem = validate(child.getDeck()); - if (conformanceProblem != null) - { - log.warn("Ignoring child: {}", conformanceProblem); - consecutiveFailures++; - return false; - } - - final long fingerprint = child.getFingerprint(); - if (alreadyGenerated.contains(fingerprint)) - { - log.warn("Ignoring child: already generated"); - consecutiveFailures++; - return false; - } - - consecutiveFailures = 0; - alreadyGenerated.add(fingerprint); - return true; - } - - @Nullable - public String validate(final Deck deck) - { - String conformanceProblem = getDeckConformanceProblem(deck); - if (conformanceProblem != null) - return "not valid in the choosen format: " + conformanceProblem; - - final int uniqueCardsCount = deck.getMain().countDistinct(); - if (uniqueCardsCount > maxUniqueCards) - return uniqueCardsCount + " unique cards, more than " + maxUniqueCards; - - if (!cardPool.canBeBuilt(deck)) - return "cannot be built with the current card pool"; - - for (final Condition condition : conditions) - { - conformanceProblem = condition.validate(deck); - if (conformanceProblem != null) - return conformanceProblem; - } - - return null; - } - - public String getDeckConformanceProblem(final Deck deck) - { - final UnplayableAICards unplayableCards = deck.getUnplayableAICards(); - if (unplayableCards.inMainDeck != 0) - { - return "contains cards marked as non-playable by AI: " - + unplayableCards.unplayable.get(DeckSection.Main).stream().map(PaperCard::getName).collect(Collectors.joining(", ")); - } - - final String conformanceProblem = format.getDeckConformanceProblem(deck); - return conformanceProblem != null ? conformanceProblem : DECK_FORMAT.getDeckConformanceProblem(deck); - } - - @Data - private class Condition - { - private final String cardName; - private final String op; - private final int count; - - @Nullable - public String validate(final Deck deck) - { - final int actualCount = deck.getMain().countByName(cardName); - - switch (op) - { - case "=": - return actualCount == count ? null : (cardName + ": expected: = " + count + ", actual: " + actualCount); - case "<=": - return actualCount <= count ? null : (cardName + ": expected: <= " + count + ", actual: " + actualCount); - default: - throw new RuntimeException("Invalid operator: " + op); - } - } - } -} diff --git a/src/main/java/fr/kevincorvisier/mtg/gdb/validation/ValidationService.java b/src/main/java/fr/kevincorvisier/mtg/gdb/validation/ValidationService.java @@ -0,0 +1,63 @@ +package fr.kevincorvisier.mtg.gdb.validation; + +import java.util.Collection; +import java.util.Map.Entry; +import java.util.stream.Collectors; + +import javax.annotation.Nullable; +import javax.validation.constraints.NotNull; + +import org.springframework.stereotype.Service; + +import forge.deck.Deck; +import forge.deck.Deck.UnplayableAICards; +import forge.deck.DeckSection; +import forge.item.PaperCard; +import fr.kevincorvisier.mtg.gdb.population.Individual; +import fr.kevincorvisier.mtg.gdb.validation.individual.IndividualValidator; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class ValidationService +{ + private final ErrorProneCardsService errorProneCards; + private final Collection<IndividualValidator> individualValidators; + + @Nullable + public String validate(final Individual child) + { + for (final IndividualValidator individualValidator : individualValidators) + { + final String conformanceProblem = individualValidator.validate(child); + if (conformanceProblem != null) + return conformanceProblem; + } + + final String conformanceProblem = validateDeck(child.getDeck()); + if (conformanceProblem != null) + return conformanceProblem; + + return null; + } + + @Nullable + public String validateDeck(@NonNull @NotNull final Deck deck) + { + final UnplayableAICards unplayableCards = deck.getUnplayableAICards(); + if (unplayableCards.inMainDeck != 0) + { + return "contains cards marked as non-playable by AI: " + + unplayableCards.unplayable.get(DeckSection.Main).stream().map(PaperCard::getName).collect(Collectors.joining(", ")); + } + + for (final Entry<PaperCard, Integer> entry : deck.getAllCardsInASinglePool()) + { + if (!errorProneCards.isValid(entry.getKey())) + return "contains an error-prone card: " + entry.getKey().getName(); + } + + return null; + } +} diff --git a/src/main/java/fr/kevincorvisier/mtg/gdb/validation/individual/CardPoolIndividualValidator.java b/src/main/java/fr/kevincorvisier/mtg/gdb/validation/individual/CardPoolIndividualValidator.java @@ -0,0 +1,27 @@ +package fr.kevincorvisier.mtg.gdb.validation.individual; + +import javax.annotation.Nullable; +import javax.validation.constraints.NotNull; + +import org.springframework.stereotype.Component; + +import fr.kevincorvisier.mtg.gdb.ai.CardPoolService; +import fr.kevincorvisier.mtg.gdb.population.Individual; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class CardPoolIndividualValidator implements IndividualValidator +{ + private final CardPoolService cardPool; + + @Override + @Nullable + public String validate(@NonNull @NotNull final Individual individual) + { + if (!cardPool.canBeBuilt(individual.getDeck())) + return "cannot be built with the current card pool"; + return null; + } +} diff --git a/src/main/java/fr/kevincorvisier/mtg/gdb/validation/individual/ConditionsIndividualValidator.java b/src/main/java/fr/kevincorvisier/mtg/gdb/validation/individual/ConditionsIndividualValidator.java @@ -0,0 +1,86 @@ +package fr.kevincorvisier.mtg.gdb.validation.individual; + +import java.util.Collection; +import java.util.HashSet; + +import javax.annotation.Nullable; +import javax.validation.constraints.NotNull; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import forge.deck.Deck; +import fr.kevincorvisier.mtg.gdb.population.Individual; +import lombok.Data; +import lombok.NonNull; + +@Component +public class ConditionsIndividualValidator implements IndividualValidator +{ + private final Collection<Condition> conditions = new HashSet<>(); + + @Value("${validation.child.conditions}") + public void setConditions(final String conditions) + { + if (conditions != null) + { + for (final String condition : StringUtils.split(conditions, ';')) + { + final String[] words = StringUtils.split(condition.trim(), ' '); + + if (words.length < 3) + throw new RuntimeException("Invalid validation condition: " + condition); + + final String cardName = StringUtils.join(words, ' ', 0, words.length - 2); + final String op = words[words.length - 2]; + final int count = Integer.parseInt(words[words.length - 1]); + + if (!StringUtils.equalsAny(op, "=", "<=")) + throw new RuntimeException("Invalid validation condition: " + condition); + + this.conditions.add(new Condition(cardName, op, count)); + } + } + } + + @Override + @Nullable + public String validate(@NonNull @NotNull final Individual individual) + { + final Deck deck = individual.getDeck(); + + for (final Condition condition : conditions) + { + final String conformanceProblem = condition.validate(deck); + if (conformanceProblem != null) + return conformanceProblem; + } + + return null; + } + + @Data + private class Condition + { + private final String cardName; + private final String op; + private final int count; + + @Nullable + public String validate(final Deck deck) + { + final int actualCount = deck.getMain().countByName(cardName); + + switch (op) + { + case "=": + return actualCount == count ? null : (cardName + ": expected: = " + count + ", actual: " + actualCount); + case "<=": + return actualCount <= count ? null : (cardName + ": expected: <= " + count + ", actual: " + actualCount); + default: + throw new RuntimeException("Invalid operator: " + op); + } + } + } +} diff --git a/src/main/java/fr/kevincorvisier/mtg/gdb/validation/individual/FormatIndividualValidator.java b/src/main/java/fr/kevincorvisier/mtg/gdb/validation/individual/FormatIndividualValidator.java @@ -0,0 +1,32 @@ +package fr.kevincorvisier.mtg.gdb.validation.individual; + +import javax.validation.constraints.NotNull; + +import org.springframework.stereotype.Component; + +import forge.deck.DeckFormat; +import forge.game.GameFormat; +import fr.kevincorvisier.mtg.gdb.population.Individual; +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class FormatIndividualValidator implements IndividualValidator +{ + private static final DeckFormat DECK_FORMAT = DeckFormat.Constructed; + private final GameFormat format; + + @Override + public String validate(@NotNull final Individual individual) + { + String conformanceProblem = format.getDeckConformanceProblem(individual.getDeck()); + if (conformanceProblem != null) + return "not valid in the choosen format: " + conformanceProblem; + + conformanceProblem = DECK_FORMAT.getDeckConformanceProblem(individual.getDeck()); + if (conformanceProblem != null) + return "not valid in the choosen format: " + conformanceProblem; + + return null; + } +} diff --git a/src/main/java/fr/kevincorvisier/mtg/gdb/validation/individual/IndividualValidator.java b/src/main/java/fr/kevincorvisier/mtg/gdb/validation/individual/IndividualValidator.java @@ -0,0 +1,12 @@ +package fr.kevincorvisier.mtg.gdb.validation.individual; + +import javax.annotation.Nullable; +import javax.validation.constraints.NotNull; + +import fr.kevincorvisier.mtg.gdb.population.Individual; + +public interface IndividualValidator +{ + @Nullable + String validate(@NotNull final Individual individual); +} diff --git a/src/main/java/fr/kevincorvisier/mtg/gdb/validation/individual/MaxUniqueCardsIndividualValidator.java b/src/main/java/fr/kevincorvisier/mtg/gdb/validation/individual/MaxUniqueCardsIndividualValidator.java @@ -0,0 +1,29 @@ +package fr.kevincorvisier.mtg.gdb.validation.individual; + +import javax.annotation.Nullable; +import javax.validation.constraints.NotNull; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import fr.kevincorvisier.mtg.gdb.population.Individual; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class MaxUniqueCardsIndividualValidator implements IndividualValidator +{ + @Value("${validation.child.max-unique-cards}") + private final int maxUniqueCards; + + @Override + @Nullable + public String validate(@NonNull @NotNull final Individual individual) + { + final int uniqueCardsCount = individual.getDeck().getMain().countDistinct(); + if (uniqueCardsCount > maxUniqueCards) + return uniqueCardsCount + " unique cards, more than " + maxUniqueCards; + return null; + } +}