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 cf5425987484d1f357657330f684645d8841a77b
parent ee0c1ac74664832f2b0b9da8e65ed313336adb0c
Author: Kevin Corvisier <git@kevincorvisier.fr>
Date:   Sun,  1 Dec 2024 21:12:01 +0900

When an exception related to a specific card stops a game, add the card
to a file listing error-prone cards
Diffstat:
Msrc/main/java/fr/kevincorvisier/mtg/gdb/ApplicationConfig.java | 13+++++++++++++
Msrc/main/java/fr/kevincorvisier/mtg/gdb/evaluation/GameSimulator.java | 5+++++
Msrc/main/java/fr/kevincorvisier/mtg/gdb/evaluation/GoldfishEvaluation.java | 5++++-
Msrc/main/java/fr/kevincorvisier/mtg/gdb/evaluation/WinRatioEvaluation.java | 6++++--
Asrc/main/java/fr/kevincorvisier/mtg/gdb/spring/converters/StringToCardListFileConverter.java | 35+++++++++++++++++++++++++++++++++++
Asrc/main/java/fr/kevincorvisier/mtg/gdb/spring/converters/StringToFileConverter.java | 26++++++++++++++++++++++++++
Asrc/main/java/fr/kevincorvisier/mtg/gdb/utils/CardListFile.java | 94+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/main/java/fr/kevincorvisier/mtg/gdb/validation/ErrorProneCardsService.java | 43+++++++++++++++++++++++++++++++++++++++++++
Msrc/main/packaged-resources/cfg/application.properties | 2++
Asrc/main/packaged-resources/cfg/error-prone-cards.txt | 1+
10 files changed, 227 insertions(+), 3 deletions(-)

diff --git a/src/main/java/fr/kevincorvisier/mtg/gdb/ApplicationConfig.java b/src/main/java/fr/kevincorvisier/mtg/gdb/ApplicationConfig.java @@ -11,11 +11,15 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.support.DefaultConversionService; import forge.game.GameFormat; import forge.game.GameFormat.FormatSubType; import forge.game.GameFormat.FormatType; import forge.model.FModel; +import fr.kevincorvisier.mtg.gdb.spring.converters.StringToCardListFileConverter; +import fr.kevincorvisier.mtg.gdb.spring.converters.StringToFileConverter; @Configuration @PropertySource("application.properties") @@ -64,4 +68,13 @@ public class ApplicationConfig return new GameFormat("MiddleSchool", MIDDLE_SCHOOL_EFFECTIVE_DATE, MIDDLE_SCHOOL_SETS, MIDDLE_SCHOOL_BANNED_CARDS, null, false, MIDDLE_SCHOOL_ADDITIONAL_CARDS, null, 0, FormatType.CUSTOM, FormatSubType.CUSTOM); } + + @Bean + public ConversionService conversionService() + { + final DefaultConversionService conversionService = new DefaultConversionService(); + conversionService.addConverter(new StringToCardListFileConverter(conversionService)); + conversionService.addConverter(new StringToFileConverter()); + return conversionService; + } } diff --git a/src/main/java/fr/kevincorvisier/mtg/gdb/evaluation/GameSimulator.java b/src/main/java/fr/kevincorvisier/mtg/gdb/evaluation/GameSimulator.java @@ -16,6 +16,7 @@ import forge.game.event.IGameEventVisitor; import forge.game.player.RegisteredPlayer; import fr.kevincorvisier.mtg.gdb.evaluation.GameSimulationResult.GameSimulationResultValue; import fr.kevincorvisier.mtg.gdb.utils.TimeLimitedCodeBlock; +import fr.kevincorvisier.mtg.gdb.validation.ErrorProneCardsService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -25,6 +26,7 @@ public class GameSimulator { private final int gameTimeoutTurns; private final int gameTimeoutSeconds; + private final ErrorProneCardsService errorProneCards; public GameSimulationResult simulateGame(final RegisteredPlayer player, final Match match) { @@ -67,7 +69,10 @@ public class GameSimulator if (e instanceof TimeoutException) log.warn("Game stopped (timeout), result={}", result); else + { log.error("Game stopped (error), result={}", result, e); + errorProneCards.handleException(e); + } return result; } diff --git a/src/main/java/fr/kevincorvisier/mtg/gdb/evaluation/GoldfishEvaluation.java b/src/main/java/fr/kevincorvisier/mtg/gdb/evaluation/GoldfishEvaluation.java @@ -14,6 +14,7 @@ import forge.game.player.RegisteredPlayer; import forge.player.GamePlayerUtil; import fr.kevincorvisier.mtg.gdb.population.Individual; import fr.kevincorvisier.mtg.gdb.population.Population; +import fr.kevincorvisier.mtg.gdb.validation.ErrorProneCardsService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -33,6 +34,8 @@ public class GoldfishEvaluation implements Evaluation DECK_GOLDFISH.getMain().add("Island", 12); } + private final ErrorProneCardsService errorProneCards; + @Value("${evaluation.goldfish.initial.min-games}") private final int initialMinGames; @Value("${evaluation.goldfish.subsequent.min-games}") @@ -81,7 +84,7 @@ public class GoldfishEvaluation implements Evaluation final Match mc = new Match(rules, Arrays.asList(player, opponent), "Test"); - final GameSimulator simulator = new GameSimulator(gameTimeoutTurns, gameTimeoutSeconds); + final GameSimulator simulator = new GameSimulator(gameTimeoutTurns, gameTimeoutSeconds, errorProneCards); do { diff --git a/src/main/java/fr/kevincorvisier/mtg/gdb/evaluation/WinRatioEvaluation.java b/src/main/java/fr/kevincorvisier/mtg/gdb/evaluation/WinRatioEvaluation.java @@ -19,6 +19,7 @@ import forge.player.GamePlayerUtil; 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 lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -31,8 +32,9 @@ public class WinRatioEvaluation implements Evaluation private Collection<Deck> opponents = Collections.emptySet(); private final MagicOnlineDeckLoader forgeUtils; + private final ErrorProneCardsService errorProneCards; - @Value("#{'${evaluation.win-ratio.opponents-directories}'.split(',')}") + @Value("${evaluation.win-ratio.opponents-directories}") private final File[] opponentsDirectories; @Value("${evaluation.win-ratio.initial.min-games}") private final int initialMinGames; @@ -98,7 +100,7 @@ public class WinRatioEvaluation implements Evaluation final Match mc = new Match(rules, Arrays.asList(player, opponent), "Test"); - final GameSimulator simulator = new GameSimulator(gameTimeoutTurns, gameTimeoutSeconds); + final GameSimulator simulator = new GameSimulator(gameTimeoutTurns, gameTimeoutSeconds, errorProneCards); do { diff --git a/src/main/java/fr/kevincorvisier/mtg/gdb/spring/converters/StringToCardListFileConverter.java b/src/main/java/fr/kevincorvisier/mtg/gdb/spring/converters/StringToCardListFileConverter.java @@ -0,0 +1,35 @@ +package fr.kevincorvisier.mtg.gdb.spring.converters; + +import java.io.File; +import java.io.IOException; + +import javax.annotation.Nullable; +import javax.validation.constraints.NotNull; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.converter.Converter; + +import fr.kevincorvisier.mtg.gdb.utils.CardListFile; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class StringToCardListFileConverter implements Converter<String, CardListFile> +{ + private final ConversionService conversionService; + + @Override + @Nullable + public CardListFile convert(@NonNull @NotNull final String source) + { + try + { + final File file = conversionService.convert(source, File.class); + return file != null ? new CardListFile(file) : null; + } + catch (final IOException e) + { + throw new IllegalArgumentException(e); + } + } +} diff --git a/src/main/java/fr/kevincorvisier/mtg/gdb/spring/converters/StringToFileConverter.java b/src/main/java/fr/kevincorvisier/mtg/gdb/spring/converters/StringToFileConverter.java @@ -0,0 +1,26 @@ +package fr.kevincorvisier.mtg.gdb.spring.converters; + +import java.io.File; + +import javax.annotation.Nullable; +import javax.validation.constraints.NotNull; + +import org.springframework.beans.propertyeditors.FileEditor; +import org.springframework.core.convert.converter.Converter; + +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class StringToFileConverter implements Converter<String, File> +{ + private final FileEditor propertyEditor = new FileEditor(); + + @Override + @Nullable + public File convert(@NonNull @NotNull final String source) + { + propertyEditor.setAsText(source); + return (File) propertyEditor.getValue(); + } +} diff --git a/src/main/java/fr/kevincorvisier/mtg/gdb/utils/CardListFile.java b/src/main/java/fr/kevincorvisier/mtg/gdb/utils/CardListFile.java @@ -0,0 +1,94 @@ +package fr.kevincorvisier.mtg.gdb.utils; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; + +import javax.validation.constraints.NotNull; + +import lombok.NonNull; +import lombok.Synchronized; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class CardListFile +{ + private final File file; + + private boolean loaded = false; + private Collection<String> list; + + public CardListFile(@NonNull @NotNull final File file) throws IOException + { + this.file = file; + load(); + } + + @Synchronized + public void add(final String name) + { + list.add(name); + log.info("add: {}", name); + save(); + } + + private void load() throws IOException + { + if (!file.exists() && list == null && !loaded) + { + list = new HashSet<>(); + loaded = true; + return; + } + + try (BufferedReader reader = new BufferedReader(new FileReader(file))) + { + final Collection<String> newList = new HashSet<>(); + + String line; + while ((line = reader.readLine()) != null) + newList.add(line.trim()); + + list = newList; + loaded = true; + } + catch (final Exception e) + { + if (list == null || !loaded) + throw e; + + log.warn("Error while reloading file {}, keeping the previous content", file.getName(), e); + } + } + + private void save() + { + final File parentFile = file.getParentFile(); + if (parentFile != null && !parentFile.exists()) + parentFile.mkdirs(); + + try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) + { + final List<String> sorted = new ArrayList<>(list); + Collections.sort(sorted); + + for (final String card : sorted) + { + writer.write(card); + writer.newLine(); + } + } + catch (final IOException e) + { + log.warn("Unable to save {}", file.getName(), e); + } + } +} diff --git a/src/main/java/fr/kevincorvisier/mtg/gdb/validation/ErrorProneCardsService.java b/src/main/java/fr/kevincorvisier/mtg/gdb/validation/ErrorProneCardsService.java @@ -0,0 +1,43 @@ +package fr.kevincorvisier.mtg.gdb.validation; + +import java.security.InvalidParameterException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.validation.constraints.NotNull; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import fr.kevincorvisier.mtg.gdb.utils.CardListFile; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ErrorProneCardsService +{ + private static final Pattern PATTERN_INVALID_PARAMETER_EXCEPTION = Pattern + .compile("^(?<name>.+) \\(\\d+\\)'s ability resulted in no types to choose from$"); + + @Value("${cards.error-prone.file}") + private final CardListFile cards; + + public void handleException(@NonNull @NotNull final Throwable e) + { + final String message = e.getMessage(); + + if (e instanceof InvalidParameterException) + { + final Matcher matcher = PATTERN_INVALID_PARAMETER_EXCEPTION.matcher(message); + if (matcher.matches()) + cards.add(matcher.group("name")); + else + log.warn("handleException: unable to handle InvalidParameterException with message: {}", message); + } + else + log.warn("handleException uable to handle {}", e.getClass().getName()); + } +} diff --git a/src/main/packaged-resources/cfg/application.properties b/src/main/packaged-resources/cfg/application.properties @@ -6,5 +6,7 @@ max.no-improvement-count=20 format=MiddleSchool card-pool=example1/card-pool.txt +cards.error-prone.file=error-prone-cards.txt + validation.child.conditions= validation.child.max-unique-cards=24 diff --git a/src/main/packaged-resources/cfg/error-prone-cards.txt b/src/main/packaged-resources/cfg/error-prone-cards.txt @@ -0,0 +1 @@ +Shared Triumph