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 55dbaeb796d71170bc7bbbc8bc8da84d829626d0
parent dbd1a610fcfc46a891f6a6e9aef61ca791ff28e4
Author: Kevin Corvisier <git@kevincorvisier.fr>
Date:   Fri, 20 Dec 2024 18:53:52 +0900

Win evaluation: add possibility to have a different number of games
performed depending on the current win-ratio of an individual
Diffstat:
Msrc/main/java/fr/kevincorvisier/mtg/gdb/ApplicationConfig.java | 2++
Asrc/main/java/fr/kevincorvisier/mtg/gdb/evaluation/WinRatioContinueCondition.java | 23+++++++++++++++++++++++
Msrc/main/java/fr/kevincorvisier/mtg/gdb/evaluation/WinRatioEvaluation.java | 57+++++++++++++++++++++++++++++++++++++++++----------------
Msrc/main/java/fr/kevincorvisier/mtg/gdb/evaluation/WinRatioEvaluationContext.java | 11+++++++----
Msrc/main/java/fr/kevincorvisier/mtg/gdb/population/Individual.java | 2+-
Asrc/main/java/fr/kevincorvisier/mtg/gdb/spring/converters/StringToWinRatioContinueConditionConverter.java | 20++++++++++++++++++++
Msrc/main/packaged-resources/cfg/evaluation.properties | 5+++++
7 files changed, 99 insertions(+), 21 deletions(-)

diff --git a/src/main/java/fr/kevincorvisier/mtg/gdb/ApplicationConfig.java b/src/main/java/fr/kevincorvisier/mtg/gdb/ApplicationConfig.java @@ -20,6 +20,7 @@ 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; +import fr.kevincorvisier.mtg.gdb.spring.converters.StringToWinRatioContinueConditionConverter; @Configuration @PropertySource("application.properties") @@ -73,6 +74,7 @@ public class ApplicationConfig final DefaultConversionService conversionService = new DefaultConversionService(); conversionService.addConverter(new StringToCardListFileConverter(conversionService)); conversionService.addConverter(new StringToFileConverter()); + conversionService.addConverter(new StringToWinRatioContinueConditionConverter()); return conversionService; } } diff --git a/src/main/java/fr/kevincorvisier/mtg/gdb/evaluation/WinRatioContinueCondition.java b/src/main/java/fr/kevincorvisier/mtg/gdb/evaluation/WinRatioContinueCondition.java @@ -0,0 +1,23 @@ +package fr.kevincorvisier.mtg.gdb.evaluation; + +import javax.validation.constraints.NotNull; + +import lombok.Data; +import lombok.NonNull; + +@Data +public class WinRatioContinueCondition +{ + private final int minPlayedGames; + private final double requiredWinRate; + + public boolean accept(@NonNull @NotNull final WinRatioEvaluationContext context) + { + return context.getGamesPlayed() >= minPlayedGames; + } + + public boolean canContinue(@NonNull @NotNull final WinRatioEvaluationContext context) + { + return context.getWinRatio() >= requiredWinRate; + } +} diff --git a/src/main/java/fr/kevincorvisier/mtg/gdb/evaluation/WinRatioEvaluation.java b/src/main/java/fr/kevincorvisier/mtg/gdb/evaluation/WinRatioEvaluation.java @@ -6,6 +6,8 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import javax.validation.constraints.NotNull; + import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Conditional; import org.springframework.stereotype.Service; @@ -48,6 +50,8 @@ public class WinRatioEvaluation implements Evaluation private final int gameTimeoutTurns; @Value("${evaluation.win-ratio.timeout.seconds}") private final int gameTimeoutSeconds; + @Value("${evaluation.win-ratio.continue-conditions}") + private final Collection<WinRatioContinueCondition> gameThreshold; @Override public void reload() @@ -84,29 +88,50 @@ public class WinRatioEvaluation implements Evaluation individual.setWinRatioContext(context); } - final int minGames = context.getGamePlayed().intValue() == 0 ? initialMinGames : subsequentMinGames; - final int gamesPerMatch = (int) Math.ceil((double) (minGames) / (double) (opponents.size())); + final int minGames = context.getGamesPlayed() == 0 ? initialMinGames : subsequentMinGames; + final int gamesPerOpponent = (int) Math.ceil((double) (minGames) / (double) (opponents.size())); final GameRules rules = new GameRules(GameType.Constructed); - rules.setGamesPerMatch(gamesPerMatch); + rules.setGamesPerMatch(1); + + log.debug("individual={}, minGames={}, opponents={}, gamesPerMatch={}", individual.getName(), minGames, opponents.size(), gamesPerOpponent); + + for (int i = 0; i != gamesPerOpponent; i++) + { + if (skipEvaluation(individual, context)) + return; - log.debug("individual={}, minGames={}, opponents={}, gamesPerMatch={}", individual.getName(), minGames, opponents.size(), gamesPerMatch); + opponents.parallelStream().forEach(opponentDeck -> { + final RegisteredPlayer player = new RegisteredPlayer(individual.getDeck()) + .setPlayer(GamePlayerUtil.createAiPlayer(individual.getName(), playerAiProfile)); + final RegisteredPlayer opponent = new RegisteredPlayer(opponentDeck) + .setPlayer(GamePlayerUtil.createAiPlayer(opponentDeck.getName(), opponentAiProfile)); - opponents.parallelStream().forEach(opponentDeck -> { - final RegisteredPlayer player = new RegisteredPlayer(individual.getDeck()) - .setPlayer(GamePlayerUtil.createAiPlayer(individual.getName(), playerAiProfile)); - final RegisteredPlayer opponent = new RegisteredPlayer(opponentDeck) - .setPlayer(GamePlayerUtil.createAiPlayer(opponentDeck.getName(), opponentAiProfile)); + final Match mc = new Match(rules, Arrays.asList(player, opponent), "Test"); - final Match mc = new Match(rules, Arrays.asList(player, opponent), "Test"); + final GameSimulator simulator = new GameSimulator(gameTimeoutTurns, gameTimeoutSeconds, errorProneCards); - final GameSimulator simulator = new GameSimulator(gameTimeoutTurns, gameTimeoutSeconds, errorProneCards); + do + { + final GameSimulationResult result = simulator.simulateGame(player, mc); + individual.getWinRatioContext().onGameOver(result); + } while (mc.getOutcomes().size() < mc.getRules().getGamesPerMatch()); + }); + } + } - do + private boolean skipEvaluation(@NotNull final Individual individual, @NotNull final WinRatioEvaluationContext context) + { + for (final WinRatioContinueCondition condition : gameThreshold) + { + if (condition.accept(context) && !condition.canContinue(context)) { - final GameSimulationResult result = simulator.simulateGame(player, mc); - individual.getWinRatioContext().onGameOver(result); - } while (mc.getOutcomes().size() < mc.getRules().getGamesPerMatch()); - }); + log.info("Skip evaluation: of {}: not machting requirement of condition {}, context={}", individual.getName(), condition, context); + return true; + } + } + + return false; } + } diff --git a/src/main/java/fr/kevincorvisier/mtg/gdb/evaluation/WinRatioEvaluationContext.java b/src/main/java/fr/kevincorvisier/mtg/gdb/evaluation/WinRatioEvaluationContext.java @@ -3,9 +3,7 @@ package fr.kevincorvisier.mtg.gdb.evaluation; import java.util.concurrent.atomic.AtomicInteger; import fr.kevincorvisier.mtg.gdb.evaluation.GameSimulationResult.GameSimulationResultValue; -import lombok.Data; -@Data public class WinRatioEvaluationContext { private final AtomicInteger gamePlayed = new AtomicInteger(); @@ -27,10 +25,15 @@ public class WinRatioEvaluationContext return gamePlayed.intValue() != 0; } + public int getGamesPlayed() + { + return gamePlayed.get(); + } + /** * Win ratio */ - public double getFitness() + public double getWinRatio() { return gameWon.doubleValue() / gamePlayed.doubleValue(); } @@ -38,6 +41,6 @@ public class WinRatioEvaluationContext @Override public String toString() { - return getFitness() + ": " + gameWon.doubleValue() + "/" + gamePlayed.doubleValue(); + return getWinRatio() + ": " + gameWon.doubleValue() + "/" + gamePlayed.doubleValue(); } } diff --git a/src/main/java/fr/kevincorvisier/mtg/gdb/population/Individual.java b/src/main/java/fr/kevincorvisier/mtg/gdb/population/Individual.java @@ -45,7 +45,7 @@ public class Individual final Collection<Double> fitnesses = new ArrayList<>(); if (winRatioContext != null && winRatioContext.hasData()) - fitnesses.add(winRatioContext.getFitness()); + fitnesses.add(winRatioContext.getWinRatio()); if (goldfishContext != null && goldfishContext.hasData()) fitnesses.add(goldfishContext.getFitness()); diff --git a/src/main/java/fr/kevincorvisier/mtg/gdb/spring/converters/StringToWinRatioContinueConditionConverter.java b/src/main/java/fr/kevincorvisier/mtg/gdb/spring/converters/StringToWinRatioContinueConditionConverter.java @@ -0,0 +1,20 @@ +package fr.kevincorvisier.mtg.gdb.spring.converters; + +import org.springframework.core.convert.converter.Converter; + +import fr.kevincorvisier.mtg.gdb.evaluation.WinRatioContinueCondition; + +public class StringToWinRatioContinueConditionConverter implements Converter<String, WinRatioContinueCondition> +{ + @Override + public WinRatioContinueCondition convert(final String source) + { + final String[] fields = source.split("\\|"); + if (fields.length != 2) + throw new IllegalArgumentException(source); + + final int minPlayedGames = Integer.parseInt(fields[0]); + final double requiredWinRate = Double.parseDouble(fields[1]); + return new WinRatioContinueCondition(minPlayedGames, requiredWinRate); + } +} diff --git a/src/main/packaged-resources/cfg/evaluation.properties b/src/main/packaged-resources/cfg/evaluation.properties @@ -52,3 +52,8 @@ evaluation.win-ratio.opponents-directories=example1/ms-opponents,example1/pm-opp evaluation.win-ratio.player.ai-profile=Default evaluation.win-ratio.opponent.ai-profile=Default + +# When a individual has played at least X games, continue evaluation only if the win ratio is at least Y +# format: X|Y,X|Y +evaluation.win-ratio.continue-conditions=10|0.2,20|0.4,30|0.6,40|0.8 +