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 ea98cd1f9631b7d5d27de969637e471df07dba79
parent 345891d6a859a544da6afdbb883e68abeab403c4
Author: Kevin Corvisier <git@kevincorvisier.fr>
Date:   Sun, 29 Sep 2024 20:49:22 +0900

Add stop condition for the evaluation phase
Diffstat:
Mpom.xml | 15+++++++++++++++
Msrc/main/java/fr/kevincorvisier/mtg/gdb/Main.java | 2+-
Msrc/main/java/fr/kevincorvisier/mtg/gdb/ai/GeneticAlgorithm.java | 13+++++++++++--
Asrc/main/java/fr/kevincorvisier/mtg/gdb/evaluation/stop/DayTime.java | 34++++++++++++++++++++++++++++++++++
Asrc/main/java/fr/kevincorvisier/mtg/gdb/evaluation/stop/DayTimeStopCondition.java | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/main/java/fr/kevincorvisier/mtg/gdb/evaluation/stop/DayTimeStopConditionEnabled.java | 15+++++++++++++++
Asrc/main/java/fr/kevincorvisier/mtg/gdb/evaluation/stop/EvaluationStopCondition.java | 11+++++++++++
Asrc/main/java/fr/kevincorvisier/mtg/gdb/evaluation/stop/EvaluationStopConditionType.java | 7+++++++
Asrc/main/java/fr/kevincorvisier/mtg/gdb/evaluation/stop/FixedCountStopCondition.java | 33+++++++++++++++++++++++++++++++++
Asrc/main/java/fr/kevincorvisier/mtg/gdb/evaluation/stop/FixedCountStopConditionEnabled.java | 15+++++++++++++++
Msrc/main/java/fr/kevincorvisier/mtg/gdb/operators/selection/BiasedRandomSelection.java | 4++--
Msrc/main/java/fr/kevincorvisier/mtg/gdb/utils/ForgeUtils.java | 14++++----------
Msrc/main/packaged-resources/cfg/evaluation.properties | 12++++++++++++
Asrc/test/java/fr/kevincorvisier/mtg/gdb/evaluation/stop/DayTimeTest.java | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
14 files changed, 276 insertions(+), 15 deletions(-)

diff --git a/pom.xml b/pom.xml @@ -16,6 +16,7 @@ <maven.compiler.source>17</maven.compiler.source> <forge.version>1.6.64</forge.version> + <junit.version>5.11.1</junit.version> <logback.version>1.2.10</logback.version> <lombok.version>1.18.22</lombok.version> <slf4j.version>1.7.33</slf4j.version> @@ -32,6 +33,14 @@ <dependencyManagement> <dependencies> <dependency> + <groupId>org.junit</groupId> + <artifactId>junit-bom</artifactId> + <version>${junit.version}</version> + <type>pom</type> + <scope>import</scope> + </dependency> + + <dependency> <groupId>org.springframework</groupId> <artifactId>spring-framework-bom</artifactId> <version>${spring.version}</version> @@ -87,6 +96,12 @@ <version>${lombok.version}</version> <scope>provided</scope> </dependency> + + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter</artifactId> + <scope>test</scope> + </dependency> </dependencies> <build> diff --git a/src/main/java/fr/kevincorvisier/mtg/gdb/Main.java b/src/main/java/fr/kevincorvisier/mtg/gdb/Main.java @@ -135,7 +135,7 @@ public class Main ga.run(); } } - catch (final Throwable e) + catch (final Exception e) { log.error("Uncatched exception in main thread, stopping", e); } diff --git a/src/main/java/fr/kevincorvisier/mtg/gdb/ai/GeneticAlgorithm.java b/src/main/java/fr/kevincorvisier/mtg/gdb/ai/GeneticAlgorithm.java @@ -13,6 +13,7 @@ import org.springframework.stereotype.Service; import fr.kevincorvisier.mtg.gdb.Reloadable; import fr.kevincorvisier.mtg.gdb.evaluation.Evaluation; +import fr.kevincorvisier.mtg.gdb.evaluation.stop.EvaluationStopCondition; import fr.kevincorvisier.mtg.gdb.operators.crossover.Crossover; import fr.kevincorvisier.mtg.gdb.operators.mutation.Mutation; import fr.kevincorvisier.mtg.gdb.operators.selection.HybridSelection; @@ -38,6 +39,7 @@ public class GeneticAlgorithm implements Reloadable private final Selection selection = new HybridSelection(); private final List<Evaluation> evaluators; private final ChildValidator childValidator; + private final EvaluationStopCondition evaluationStoppingCondition; private Population population = null; @@ -105,8 +107,15 @@ public class GeneticAlgorithm implements Reloadable else toNextGeneration(); - for (final Evaluation evaluator : evaluators) - evaluator.evaluateFitness(population); + evaluationStoppingCondition.reset(); + while (evaluationStoppingCondition.keepEvaluating()) + { + log.info("Starting evaluation round"); + for (final Evaluation evaluator : evaluators) + evaluator.evaluateFitness(population); + log.info("Ending evaluation round"); + } + population.onFitnessUpdated(); writeOutput(); diff --git a/src/main/java/fr/kevincorvisier/mtg/gdb/evaluation/stop/DayTime.java b/src/main/java/fr/kevincorvisier/mtg/gdb/evaluation/stop/DayTime.java @@ -0,0 +1,34 @@ +package fr.kevincorvisier.mtg.gdb.evaluation.stop; + +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoField; +import java.time.temporal.TemporalAccessor; +import java.util.Locale; + +import lombok.Data; + +@Data +/* package */ class DayTime +{ + private static final DateTimeFormatter FORMATTER_SCHEDULE_ITEM = DateTimeFormatter.ofPattern("E - HH:mm", Locale.ENGLISH); + + private final int dayOfWeek; + private final int minuteOfDay; + + public DayTime(final String s, final ZoneId timeZone) + { + final TemporalAccessor zonedTemporalAccessor = FORMATTER_SCHEDULE_ITEM.withZone(timeZone).parse(s.trim()); + dayOfWeek = zonedTemporalAccessor.get(ChronoField.DAY_OF_WEEK); + minuteOfDay = zonedTemporalAccessor.get(ChronoField.MINUTE_OF_DAY); + } + + public ZonedDateTime getNext(final ZonedDateTime after) + { + final ZonedDateTime next = after.with(ChronoField.DAY_OF_WEEK, dayOfWeek).with(ChronoField.MINUTE_OF_DAY, minuteOfDay) // + .with(ChronoField.SECOND_OF_MINUTE, 0).with(ChronoField.MILLI_OF_SECOND, 0); + + return next.isBefore(after) ? next.plusWeeks(1) : next; + } +} diff --git a/src/main/java/fr/kevincorvisier/mtg/gdb/evaluation/stop/DayTimeStopCondition.java b/src/main/java/fr/kevincorvisier/mtg/gdb/evaluation/stop/DayTimeStopCondition.java @@ -0,0 +1,65 @@ +package fr.kevincorvisier.mtg.gdb.evaluation.stop; + +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.HashSet; +import java.util.Set; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Conditional; +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * Stops the evaluation phase after exceeding one of the configured limit (day of week + time) + */ +@Slf4j +@Service +@RequiredArgsConstructor +@Conditional(DayTimeStopConditionEnabled.class) +public class DayTimeStopCondition implements EvaluationStopCondition +{ + @Value("${evaluation.stop-condition.day-time.timezone}") + private final ZoneId timeZone; + + private final Set<DayTime> limits = new HashSet<>(); + + private ZonedDateTime stopDateTime; + + @Value("${evaluation.stop-condition.day-time.limits}") + public void setSchedule(final String[] scheduleItemsStr) + { + limits.clear(); + + for (final String scheduleItemStr : scheduleItemsStr) + limits.add(new DayTime(scheduleItemStr, timeZone)); + } + + @Override + public boolean keepEvaluating() + { + final ZonedDateTime now = ZonedDateTime.now(timeZone); + + if (stopDateTime == null) + { + for (final DayTime item : limits) + { + final ZonedDateTime limitNext = item.getNext(now); + if (stopDateTime == null || limitNext.isBefore(stopDateTime)) + stopDateTime = limitNext; + } + + log.info("Stop date/time: {}", stopDateTime); + } + + return now.isBefore(stopDateTime); + } + + @Override + public void reset() + { + stopDateTime = null; + } +} diff --git a/src/main/java/fr/kevincorvisier/mtg/gdb/evaluation/stop/DayTimeStopConditionEnabled.java b/src/main/java/fr/kevincorvisier/mtg/gdb/evaluation/stop/DayTimeStopConditionEnabled.java @@ -0,0 +1,15 @@ +package fr.kevincorvisier.mtg.gdb.evaluation.stop; + +import org.springframework.context.annotation.Condition; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.core.type.AnnotatedTypeMetadata; + +public class DayTimeStopConditionEnabled implements Condition +{ + @Override + public boolean matches(final ConditionContext context, final AnnotatedTypeMetadata metadata) + { + return context.getEnvironment().getProperty("evaluation.stop-condition.type", + EvaluationStopConditionType.class) == EvaluationStopConditionType.DAY_TIME; + } +} diff --git a/src/main/java/fr/kevincorvisier/mtg/gdb/evaluation/stop/EvaluationStopCondition.java b/src/main/java/fr/kevincorvisier/mtg/gdb/evaluation/stop/EvaluationStopCondition.java @@ -0,0 +1,11 @@ +package fr.kevincorvisier.mtg.gdb.evaluation.stop; + +/** + * Define the condition ending the evaluation phase of each generation + */ +public interface EvaluationStopCondition +{ + boolean keepEvaluating(); + + void reset(); +} diff --git a/src/main/java/fr/kevincorvisier/mtg/gdb/evaluation/stop/EvaluationStopConditionType.java b/src/main/java/fr/kevincorvisier/mtg/gdb/evaluation/stop/EvaluationStopConditionType.java @@ -0,0 +1,7 @@ +package fr.kevincorvisier.mtg.gdb.evaluation.stop; + +public enum EvaluationStopConditionType +{ + DAY_TIME, + FIXED_COUNT +} diff --git a/src/main/java/fr/kevincorvisier/mtg/gdb/evaluation/stop/FixedCountStopCondition.java b/src/main/java/fr/kevincorvisier/mtg/gdb/evaluation/stop/FixedCountStopCondition.java @@ -0,0 +1,33 @@ +package fr.kevincorvisier.mtg.gdb.evaluation.stop; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Conditional; +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; + +/** + * Stops the evaluation phase after a configured number of cycles + */ +@Service +@RequiredArgsConstructor +@Conditional(FixedCountStopConditionEnabled.class) +public class FixedCountStopCondition implements EvaluationStopCondition +{ + @Value("${evaluation.stop-condition.fixed-count.target}") + private final int target; + + private int count = 0; + + @Override + public boolean keepEvaluating() + { + return count++ < target; + } + + @Override + public void reset() + { + count = 0; + } +} diff --git a/src/main/java/fr/kevincorvisier/mtg/gdb/evaluation/stop/FixedCountStopConditionEnabled.java b/src/main/java/fr/kevincorvisier/mtg/gdb/evaluation/stop/FixedCountStopConditionEnabled.java @@ -0,0 +1,15 @@ +package fr.kevincorvisier.mtg.gdb.evaluation.stop; + +import org.springframework.context.annotation.Condition; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.core.type.AnnotatedTypeMetadata; + +public class FixedCountStopConditionEnabled implements Condition +{ + @Override + public boolean matches(final ConditionContext context, final AnnotatedTypeMetadata metadata) + { + return context.getEnvironment().getProperty("evaluation.stop-condition.type", + EvaluationStopConditionType.class) == EvaluationStopConditionType.FIXED_COUNT; + } +} diff --git a/src/main/java/fr/kevincorvisier/mtg/gdb/operators/selection/BiasedRandomSelection.java b/src/main/java/fr/kevincorvisier/mtg/gdb/operators/selection/BiasedRandomSelection.java @@ -18,8 +18,8 @@ public class BiasedRandomSelection extends Selection // Normalize fitness values to sum to 1 // This allows us to randomly generate a number between 0-1 and select that individual // [0.25, 0.30, 0.45] - final double sum = population.stream().map(Individual::getFitness).reduce((a, b) -> a + b).get(); - final List<Double> normalizedProportions = population.stream().map(Individual::getFitness).map(f -> f / sum).collect(Collectors.toUnmodifiableList()); + final double sum = population.stream().collect(Collectors.summingDouble(Individual::getFitness)); + final List<Double> normalizedProportions = population.stream().map(Individual::getFitness).map(f -> f / sum).toList(); // Create a list to hold our cumulated values final List<Double> cumulativeProportions = new ArrayList<>(); diff --git a/src/main/java/fr/kevincorvisier/mtg/gdb/utils/ForgeUtils.java b/src/main/java/fr/kevincorvisier/mtg/gdb/utils/ForgeUtils.java @@ -3,7 +3,6 @@ package fr.kevincorvisier.mtg.gdb.utils; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; -import java.io.FilenameFilter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -66,13 +65,8 @@ public class ForgeUtils private static Collection<Deck> loadDecksFromDirectory(final File directory) { - final File[] deckFiles = directory.listFiles(new FilenameFilter() - { - @Override - public boolean accept(final File dir, final String name) - { - return StringUtils.endsWith(name, ".txt"); - } + final File[] deckFiles = directory.listFiles((final File dir, final String name) -> { + return StringUtils.endsWith(name, ".txt"); }); final Collection<Deck> decks = new ArrayList<>(); @@ -120,7 +114,7 @@ public class ForgeUtils currentSection.add(card, amount); } } - catch (final Throwable e) + catch (final Exception e) { log.warn("Unable to parse file: {}", file, e); return null; @@ -171,7 +165,7 @@ public class ForgeUtils result.add(card); } } - catch (final Throwable e) + catch (final Exception e) { log.warn("Unable to parse file: {}", from, e); return null; diff --git a/src/main/packaged-resources/cfg/evaluation.properties b/src/main/packaged-resources/cfg/evaluation.properties @@ -1,5 +1,17 @@ +# When to stop the evaluation phase of each generation, possible values: FIXED_COUNT, DAY_TIME +evaluation.stop-condition.type=FIXED_COUNT + +# List of day/time limits, when the limit is exceeded, the evaluation phase will end +evaluation.stop-condition.day-time.limits=Sun - 16:00, Sun - 16:05 +# Timezone for the day/time limits +evaluation.stop-condition.day-time.timezone=Asia/Tokyo + + +# Number of evaluation cycle to perform before stopping +evaluation.stop-condition.fixed-count.target=1 + # # Goldfish evaluation diff --git a/src/test/java/fr/kevincorvisier/mtg/gdb/evaluation/stop/DayTimeTest.java b/src/test/java/fr/kevincorvisier/mtg/gdb/evaluation/stop/DayTimeTest.java @@ -0,0 +1,51 @@ +package fr.kevincorvisier.mtg.gdb.evaluation.stop; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.time.ZoneId; +import java.time.ZonedDateTime; + +import org.junit.jupiter.api.Test; + +class DayTimeTest +{ + private static final ZoneId TZ = ZoneId.of("Asia/Tokyo"); + + @Test + void sunday() + { + final ZonedDateTime expected = ZonedDateTime.of(2024, 9, 29, 17, 0, 0, 0, TZ); + final DayTime dayTime = new DayTime("Sun - 17:00", TZ); + + // Right after the previous schedule item + assertEquals(expected, dayTime.getNext(ZonedDateTime.of(2024, 9, 22, 17, 0, 0, 1, TZ))); + // Right before the schedule item + assertEquals(expected, dayTime.getNext(ZonedDateTime.of(2024, 9, 29, 16, 59, 59, 999, TZ))); + + assertEquals(expected, dayTime.getNext(ZonedDateTime.of(2024, 9, 23, 17, 0, 0, 0, TZ))); + assertEquals(expected, dayTime.getNext(ZonedDateTime.of(2024, 9, 24, 17, 0, 0, 0, TZ))); + assertEquals(expected, dayTime.getNext(ZonedDateTime.of(2024, 9, 25, 17, 0, 0, 0, TZ))); + assertEquals(expected, dayTime.getNext(ZonedDateTime.of(2024, 9, 26, 17, 0, 0, 0, TZ))); + assertEquals(expected, dayTime.getNext(ZonedDateTime.of(2024, 9, 27, 17, 0, 0, 0, TZ))); + assertEquals(expected, dayTime.getNext(ZonedDateTime.of(2024, 9, 28, 17, 0, 0, 0, TZ))); + } + + @Test + void monday() + { + final ZonedDateTime expected = ZonedDateTime.of(2024, 9, 30, 17, 0, 0, 0, TZ); + final DayTime dayTime = new DayTime("Mon - 17:00", TZ); + + // Right after the previous schedule item + assertEquals(expected, dayTime.getNext(ZonedDateTime.of(2024, 9, 23, 17, 0, 0, 1, TZ))); + // Right before the schedule item + assertEquals(expected, dayTime.getNext(ZonedDateTime.of(2024, 9, 30, 16, 59, 59, 999, TZ))); + + assertEquals(expected, dayTime.getNext(ZonedDateTime.of(2024, 9, 24, 17, 0, 0, 0, TZ))); + assertEquals(expected, dayTime.getNext(ZonedDateTime.of(2024, 9, 25, 17, 0, 0, 0, TZ))); + assertEquals(expected, dayTime.getNext(ZonedDateTime.of(2024, 9, 26, 17, 0, 0, 0, TZ))); + assertEquals(expected, dayTime.getNext(ZonedDateTime.of(2024, 9, 27, 17, 0, 0, 0, TZ))); + assertEquals(expected, dayTime.getNext(ZonedDateTime.of(2024, 9, 28, 17, 0, 0, 0, TZ))); + assertEquals(expected, dayTime.getNext(ZonedDateTime.of(2024, 9, 29, 17, 0, 0, 0, TZ))); + } +}