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 30338046d9c1c07e54e6e78bd266e9955befd136
parent 5884d1deef322fbe8fd5a2bfa92a0d158550942b
Author: Kevin Corvisier <git@kevincorvisier.fr>
Date:   Tue, 17 Dec 2024 18:28:14 +0900

Mutation: decide number of cards to swap based on parents average
fitness, use probability for a card to be removed
Diffstat:
Msrc/main/java/fr/kevincorvisier/mtg/gdb/operators/mutation/Mutation.java | 2+-
Msrc/main/java/fr/kevincorvisier/mtg/gdb/operators/mutation/RandomCardMutation.java | 27+++++++++++++++++++++++----
Msrc/main/java/fr/kevincorvisier/mtg/gdb/operators/mutation/SwapSideboardMutation.java | 2+-
Msrc/main/java/fr/kevincorvisier/mtg/gdb/population/NextGenerationService.java | 4+++-
4 files changed, 28 insertions(+), 7 deletions(-)

diff --git a/src/main/java/fr/kevincorvisier/mtg/gdb/operators/mutation/Mutation.java b/src/main/java/fr/kevincorvisier/mtg/gdb/operators/mutation/Mutation.java @@ -4,5 +4,5 @@ import fr.kevincorvisier.mtg.gdb.population.Individual; public interface Mutation { - void mutate(final Individual individual); + void mutate(final Individual individual, final double estimatedFitness); } diff --git a/src/main/java/fr/kevincorvisier/mtg/gdb/operators/mutation/RandomCardMutation.java b/src/main/java/fr/kevincorvisier/mtg/gdb/operators/mutation/RandomCardMutation.java @@ -6,7 +6,7 @@ import org.springframework.context.annotation.Conditional; import org.springframework.stereotype.Service; import forge.deck.CardPool; -import forge.util.Aggregates; +import forge.item.PaperCard; import fr.kevincorvisier.mtg.gdb.ai.CardPoolService; import fr.kevincorvisier.mtg.gdb.population.Individual; import lombok.RequiredArgsConstructor; @@ -21,12 +21,14 @@ import lombok.RequiredArgsConstructor; private final CardPoolService cardPool; @Override - public void mutate(final Individual individual) + public void mutate(final Individual individual, final double estimatedFitness) { - final int nbCards = rnd.nextInt(12) + 1; + final double cardSwapProbability = getSwapCardCount(estimatedFitness) / individual.getDeck().getMain().countAll(); final CardPool main = individual.getDeck().getMain(); - main.removeAllFlat(Aggregates.random(main.toFlatList(), nbCards)); + for (final PaperCard card : main.toFlatList()) + if (rnd.nextDouble() <= cardSwapProbability) + main.remove(card); int toAddCount; @@ -46,6 +48,23 @@ import lombok.RequiredArgsConstructor; } } + /** + * Decide the number of cards to swap. Smaller estimated fitness leads to more cards being swapped + */ + private static double getSwapCardCount(final double estimatedFitness) + { + if (estimatedFitness >= 80d) + return 1d; + else if (estimatedFitness >= 60d) + return 2d; + else if (estimatedFitness >= 40d) + return 4d; + else if (estimatedFitness >= 20d) + return 8d; + else + return 16d; + } + private static boolean isBasicLand(final String card) { return card.equals("Swamp") || card.equals("Plains") || card.equals("Island") || card.equals("Forest") || card.equals("Mountain") diff --git a/src/main/java/fr/kevincorvisier/mtg/gdb/operators/mutation/SwapSideboardMutation.java b/src/main/java/fr/kevincorvisier/mtg/gdb/operators/mutation/SwapSideboardMutation.java @@ -21,7 +21,7 @@ import lombok.RequiredArgsConstructor; private final Random rnd = new Random(); @Override - public void mutate(final Individual individual) + public void mutate(final Individual individual, final double estimatedFitness) { int nbCardsToSwap = rnd.nextInt(8) + 1; final int sideboardSize = individual.getDeck().getOrCreate(DeckSection.Sideboard).countAll(); diff --git a/src/main/java/fr/kevincorvisier/mtg/gdb/population/NextGenerationService.java b/src/main/java/fr/kevincorvisier/mtg/gdb/population/NextGenerationService.java @@ -3,6 +3,7 @@ package fr.kevincorvisier.mtg.gdb.population; import java.util.Collection; import java.util.HashSet; import java.util.List; +import java.util.stream.Collectors; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @@ -80,6 +81,7 @@ public class NextGenerationService { // Selection final List<Individual> parents = selection.select(population); + final double estimatedFitness = parents.stream().collect(Collectors.averagingDouble(Individual::getFitness)); // Crossover for (final Individual child : crossover.crossover(parents.get(0), parents.get(1))) @@ -88,7 +90,7 @@ public class NextGenerationService continue; // Ensure second child will not breach the population limit // Mutation - mutation.mutate(child); + mutation.mutate(child, estimatedFitness); if (validationService.validate(child, next)) next.addIndividual(child);