Drafts updated with the latest match history; Draft history evaluators code rearranged

This commit is contained in:
jakub.salavec
2025-03-31 14:14:39 +02:00
parent 0229f67dfe
commit 92bd7dae4b
9 changed files with 3166 additions and 5377 deletions

View File

@@ -0,0 +1,177 @@
package com.gempukku.lotro.draft3.evaluator;
import com.gempukku.lotro.common.Side;
import com.gempukku.lotro.game.CardNotFoundException;
import com.gempukku.lotro.game.LotroCardBlueprintLibrary;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
public abstract class AbstractDraftCardEvaluator {
private static final int RARES_IN_PACK = 1;
private static final int UNCOMMONS_IN_PACK = 3;
private static final int COMMONS_IN_PACK = 7;
private final LotroCardBlueprintLibrary library;
public AbstractDraftCardEvaluator(LotroCardBlueprintLibrary library) {
this.library = library;
}
public abstract Map<String, Double> getAverageStartingCardMap();
// This is based on what I cut cards from draft pool
public final Map<String, Double> getPlayRateMap(Map<String, Integer> countMap, int gamesAnalyzed) {
return countMap.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().doubleValue() / (gamesAnalyzed * 2))); // Every game has two decks
}
// This is what bots pick according to
public final Map<String, Double> getValuesMap(Map<String, Integer> winningMap, Map<String, Integer> losingMap, int gamesAnalyzed) {
// Merge win and lose maps (they are kept separate for potential wr info)
Map<String, Integer> mergedMap = mergeMaps(winningMap, losingMap);
// Lower count of cards from Draft Packs
Map<String, Double> startingCollectionNormalizedCardCountMap = normalizeCountByDraftPackChance(mergedMap, gamesAnalyzed);
// Normalize for uneven set size
Map<String, Double> setNormalizedCardCountMap = normalizeCountBySetSize(startingCollectionNormalizedCardCountMap);
// Normalize count based on rarities
Map<String, Double> normalizedCardCountMap = normalizeCountByRarity(setNormalizedCardCountMap, library);
// Boost rarities (more likely to pick rares and uncommons)
Map<String, Double> rareInflatedMap = inflateRarity(normalizedCardCountMap, library);
// Boost FP cards (more likely to pick FP card in mixed draft)
Map<String, Double> fpInflatedMap = inflateFp(rareInflatedMap, library);
// Make sure all values are positive and in 0-1 range
Map<String, Double> shiftedMap = shift(fpInflatedMap);
return shiftedMap;
}
private static Map<String, Integer> mergeMaps(Map<String, Integer> map1, Map<String, Integer> map2) {
Map<String, Integer> result = new HashMap<>(map1);
map2.forEach((key, value) -> result.merge(key, value, Integer::sum));
return result;
}
private Map<String, Double> normalizeCountByDraftPackChance(Map<String, Integer> map, int gamesAnalyzed) {
Map<String, Double> normalizedMap = new HashMap<>();
map.forEach((key, value) -> {
double cardsGiven = gamesAnalyzed * 2 * getAverageStartingCardMap().getOrDefault(key, 0.0);
normalizedMap.put(key, value.doubleValue() - (cardsGiven / 1.5)); // Assume 75 % of assigned cards are played, rest is drafted
});
return normalizedMap;
}
private Map<String, Double> normalizeCountBySetSize(Map<String, Double> map) {
Map<String, Double> normalizedMap = new HashMap<>();
map.forEach((key, value) -> {
if (key.startsWith("2_") || key.startsWith("3_") || key.startsWith("5_") || key.startsWith("6_")) {
normalizedMap.put(key, value / 3.0); // MoM and RotEL are 1/3 of FotR set
} else {
normalizedMap.put(key, value);
}
});
return normalizedMap;
}
private Map<String, Double> normalizeCountByRarity(Map<String, Double> map, LotroCardBlueprintLibrary library) {
Map<String, Double> normalizedMap = new HashMap<>();
map.entrySet().forEach(entry -> {
// Do not add P cards
try {
if (library.getLotroCardBlueprint(entry.getKey()).getCardInfo().rarity.equals("P")) {
return;
}
} catch (CardNotFoundException ignored) {
}
normalizedMap.put(entry.getKey(), getRarityNormalizedValue(library, entry));
});
return normalizedMap;
}
private double getRarityNormalizedValue(LotroCardBlueprintLibrary library, Map.Entry<String, Double> entry) {
double rarityInPack = 1.0;
try {
rarityInPack = switch (library.getLotroCardBlueprint(entry.getKey()).getCardInfo().rarity) {
case "R" -> RARES_IN_PACK;
case "U" -> UNCOMMONS_IN_PACK;
case "C" -> COMMONS_IN_PACK;
default -> 1.0; // Fallback for unknown cards and promos
};
} catch (CardNotFoundException ignore) {
}
// Normalize by expected rarity frequency
return entry.getValue() / rarityInPack;
}
private Map<String, Double> inflateRarity(Map<String, Double> map, LotroCardBlueprintLibrary library) {
Map<String, Double> rarityInflatedMap = new HashMap<>();
map.forEach((key, value) -> {
try {
int rarityInPack = switch (library.getLotroCardBlueprint(key).getCardInfo().rarity) {
case "R" -> RARES_IN_PACK;
case "U" -> UNCOMMONS_IN_PACK;
case "C" -> COMMONS_IN_PACK;
default -> 1; // Fallback for unknown cards and promos
};
rarityInflatedMap.put(key, value / rarityInPack);
} catch (CardNotFoundException e) {
rarityInflatedMap.put(key, value);
}
});
return rarityInflatedMap;
}
private Map<String, Double> inflateFp(Map<String, Double> map, LotroCardBlueprintLibrary library) {
Map<String, Double> fpInflatedMap = new HashMap<>();
map.forEach((key, value) -> {
try {
double sideFactor = Side.FREE_PEOPLE.equals(library.getLotroCardBlueprint(key).getSide()) ? 1.1 : 1.0;
fpInflatedMap.put(key, value * sideFactor);
} catch (CardNotFoundException e) {
fpInflatedMap.put(key, value);
}
});
return fpInflatedMap;
}
private Map<String, Double> shift(Map<String, Double> map) {
double min = map.values().stream().min(Double::compareTo).orElse(0.0);
double positiveShift;
if (min < 0) {
positiveShift = -min + 5;
} else {
positiveShift = 5;
}
Map<String, Double> positiveMap = new HashMap<>();
map.forEach((key, value) -> positiveMap.put(key, value + positiveShift));
double max = positiveMap.values().stream().max(Double::compareTo).orElse(1.0);
Map<String, Double> tbr = new HashMap<>();
positiveMap.forEach((key, value) -> tbr.put(key, value / max));
return tbr;
}
}

View File

@@ -0,0 +1,156 @@
package com.gempukku.lotro.draft3.evaluator;
import com.gempukku.lotro.common.AppConfig;
import com.gempukku.lotro.game.LotroCardBlueprintLibrary;
import com.gempukku.lotro.game.ReplayMetadata;
import com.gempukku.util.JsonUtils;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Stream;
/**
* Reads all replay summaries, filters the one we are interested in, and prints analysis to console.
* Intended use:
* 1) analyze draft games, figure out pick rates of cards so the draft bots have something to based their picks on,
* 2) analyze draft games, figure out play rates of cards, so we can cut bad cards from draft pools.
* Should be use only when needed to alter draft json files, not at every server startup.
*/
public class Evaluator {
static class Assignment {
public final Map<String, Integer> winningMap = new HashMap<>();
public final Map<String, Integer> losingMap = new HashMap<>();
public final Map<String, Integer> deckCount = new HashMap<>();
public int gamesAnalyzed = 0;
public final String format;
public final String tournament;
public Assignment(String format, String tournament) {
this.format = format;
this.tournament = tournament.toLowerCase();
}
}
/**
* Call this to analyze the games - ran for ~ 10 minutes locally in docker
* @param library library instance
*/
public static void readFiles(LotroCardBlueprintLibrary library) {
Path summaryDir = Paths.get(AppConfig.getProperty("application.root"), "replay", "summaries");
List<Assignment> assignmentList = new ArrayList<>();
// Define what we care for
assignmentList.add(new Assignment("Limited - FOTR", "draft"));
assignmentList.add(new Assignment("Limited - TTT", "draft"));
System.out.println("Game history reading started at " + new SimpleDateFormat("HH.mm.ss").format(new Date()));
// Load decks from the past
try (Stream<Path> paths = Files.walk(summaryDir)) {
paths.filter(Files::isRegularFile) // Keep only regular files
.filter(path -> path.toString().endsWith(".json")) // Filter only JSON files
.forEach(path -> analyzeSummary(path, assignmentList)); // Process each JSON file
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("Game history reading finished at " + new SimpleDateFormat("HH.mm.ss").format(new Date()));
FotrDraftCardEvaluator fotrDraftCardEvaluator = new FotrDraftCardEvaluator(library);
TttDraftCardEvaluator tttDraftCardEvaluator = new TttDraftCardEvaluator(library);
// Send the loaded data to card evaluators -> they convert card counts to values from 0.0 to 1.0
// This is what bots pick according to in FotR drafts
var fotrValues = fotrDraftCardEvaluator.getValuesMap(assignmentList.getFirst().winningMap, assignmentList.get(0).losingMap, assignmentList.getFirst().gamesAnalyzed);
// Those are values to base fotr draft pools cuts on
var fotrPlayRates = fotrDraftCardEvaluator.getPlayRateMap(assignmentList.getFirst().deckCount, assignmentList.getFirst().gamesAnalyzed);
// This is what bots pick according to in TTT drafts
var tttValues = tttDraftCardEvaluator.getValuesMap(assignmentList.get(1).winningMap, assignmentList.get(1).losingMap, assignmentList.get(1).gamesAnalyzed);
// Those are values to base TTT draft pools cuts on
var tttPlayRates = tttDraftCardEvaluator.getPlayRateMap(assignmentList.get(1).deckCount, assignmentList.get(1).gamesAnalyzed);
// Data from solo drafts do not contain rare one ring cards, don't forget to add some value manually after
System.out.println("===FOTR VALUES");
printJsonStyle(fotrValues); // For json file
printMapStyle(fotrValues); // For cached maps in evaluators (not being used right now)
System.out.println("===FOTR PLAY RATES");
// Regular draft - all rares included, U/C with play rate lower than 0.01 are cut
// Power draft - R with play rate lower than 0.02 and U/C with play rate lower than 0.05 are cut
printMapStyle(fotrPlayRates);
System.out.println("===TTT VALUES");
printJsonStyle(tttValues);
printMapStyle(tttValues);
System.out.println("===TTT PLAY RATES");
printMapStyle(tttPlayRates);
}
private static void analyzeSummary(Path jsonFile, List<Assignment> assignmentList) {
try {
// Read JSON file into a string
String json = Files.readString(jsonFile);
// Convert JSON to object
ReplayMetadata summary = JsonUtils.Convert(json, ReplayMetadata.class);
assignmentList.forEach(assignment -> {
// Look at only games assignment cares about
if (summary.GameReplayInfo != null && assignment.format.equals(summary.GameReplayInfo.format_name) && summary.GameReplayInfo.tournament.toLowerCase().contains(assignment.tournament)) {
ReplayMetadata.DeckMetadata winningDeck = summary.Decks.get(summary.GameReplayInfo.winner);
ReplayMetadata.DeckMetadata losingDeck = summary.Decks.get(summary.GameReplayInfo.loser);
if (winningDeck != null && losingDeck != null) {
assignment.gamesAnalyzed++;
assignment.deckCount.merge(winningDeck.RingBearer.replace("*", "").replace("T", ""), 1, Integer::sum);
assignment.deckCount.merge(winningDeck.Ring.replace("*", "").replace("T", ""), 1, Integer::sum);
Set<String> winningCardSet = new HashSet<>(winningDeck.DrawDeck);
winningCardSet.forEach(card -> assignment.deckCount.merge(card.replace("*", "").replace("T", ""), 1, Integer::sum));
assignment.deckCount.merge(losingDeck.RingBearer.replace("*", "").replace("T", ""), 1, Integer::sum);
assignment.deckCount.merge(losingDeck.Ring.replace("*", "").replace("T", ""), 1, Integer::sum);
Set<String> losingCardSet = new HashSet<>(losingDeck.DrawDeck);
losingCardSet.forEach(card -> assignment.deckCount.merge(card.replace("*", "").replace("T", ""), 1, Integer::sum));
assignment.winningMap.merge(winningDeck.RingBearer.replace("*", "").replace("T", ""), 1, Integer::sum);
assignment.winningMap.merge(winningDeck.Ring.replace("*", "").replace("T", ""), 1, Integer::sum);
winningDeck.DrawDeck.forEach(card -> assignment.winningMap.merge(card.replace("*", "").replace("T", ""), 1, Integer::sum));
assignment.losingMap.merge(losingDeck.RingBearer.replace("*", "").replace("T", ""), 1, Integer::sum);
assignment.losingMap.merge(losingDeck.Ring.replace("*", "").replace("T", ""), 1, Integer::sum);
losingDeck.DrawDeck.forEach(card -> assignment.losingMap.merge(card.replace("*", "").replace("T", ""), 1, Integer::sum));
}
}
});
} catch (IOException e) {
e.printStackTrace(); // Handle file reading exceptions
}
}
private static void printJsonStyle(Map<String, Double> toPrint) {
// {"card": "1_90" , "value": 0.9832224243314389 },
toPrint.entrySet().stream()
.sorted((o1, o2) -> o2.getValue().compareTo(o1.getValue()))
.forEach(stringDoubleEntry -> {
String s = "{\"card\": \"" +
stringDoubleEntry.getKey() +
"\", \"value\": " +
stringDoubleEntry.getValue() +
"},";
System.out.println(s);
});
}
private static void printMapStyle(Map<String, Double> toPrint) {
// tbr.put("4_179", 0.8343093340676018);
toPrint.entrySet().stream()
.sorted((o1, o2) -> o2.getValue().compareTo(o1.getValue()))
.forEach(stringDoubleEntry -> {
String s = "tbr.put(\"" +
stringDoubleEntry.getKey() +
"\", " +
stringDoubleEntry.getValue() +
");";
System.out.println(s);
});
}
}