From 21b4afe89007486e8acc7b705fd302e2bfa4bece Mon Sep 17 00:00:00 2001 From: Dan Date: Thu, 16 Jun 2022 10:32:57 +0100 Subject: [PATCH] - 0.1 working version --- FABStarterDeckGen/DeckConfig.cs | 42 +++ FABStarterDeckGen/DeckGenerator.cs | 325 +++++++++++++++++++++ FABStarterDeckGen/FABCollectionReader.cs | 80 +++++ FABStarterDeckGen/FabStarterDeckGen.csproj | 24 ++ FABStarterDeckGen/Program.cs | 18 ++ FABStarterDeckGen/ReferenceCardLoader.cs | 178 +++++++++++ FabStarterDeckGen.sln | 16 + 7 files changed, 683 insertions(+) create mode 100644 FABStarterDeckGen/DeckConfig.cs create mode 100644 FABStarterDeckGen/DeckGenerator.cs create mode 100644 FABStarterDeckGen/FABCollectionReader.cs create mode 100644 FABStarterDeckGen/FabStarterDeckGen.csproj create mode 100644 FABStarterDeckGen/Program.cs create mode 100644 FABStarterDeckGen/ReferenceCardLoader.cs create mode 100644 FabStarterDeckGen.sln diff --git a/FABStarterDeckGen/DeckConfig.cs b/FABStarterDeckGen/DeckConfig.cs new file mode 100644 index 0000000..3229db1 --- /dev/null +++ b/FABStarterDeckGen/DeckConfig.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; + +using System.Globalization; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace FabStarterDeckGen +{ + public partial class DeckConfig + { + [JsonProperty("Hero")] + public string Hero { get; set; } + + [JsonProperty("Core")] + public List Core { get; set; } + + [JsonProperty("Variables")] + public List Variables { get; set; } + } + + public partial class CardSpec + { + [JsonProperty("Name")] + public string Name { get; set; } + + [JsonProperty("IsPitchless")] + public bool IsPitchless { get; set; } + + [JsonProperty("Count", NullValueHandling = NullValueHandling.Ignore)] + public string Count { get; set; } + + [JsonProperty("R", NullValueHandling = NullValueHandling.Ignore)] + public string R { get; set; } + + [JsonProperty("Y", NullValueHandling = NullValueHandling.Ignore)] + public string Y { get; set; } + + [JsonProperty("B", NullValueHandling = NullValueHandling.Ignore)] + public string B { get; set; } + } +} diff --git a/FABStarterDeckGen/DeckGenerator.cs b/FABStarterDeckGen/DeckGenerator.cs new file mode 100644 index 0000000..7eaf44f --- /dev/null +++ b/FABStarterDeckGen/DeckGenerator.cs @@ -0,0 +1,325 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; + +namespace FabStarterDeckGen +{ + using System; + using System.Collections.Generic; + + using System.Globalization; + using Newtonsoft.Json; + using Newtonsoft.Json.Converters; + + public partial class DeckConfig + { + [JsonProperty("Name")] + public string Name { get; set; } + + [JsonProperty("Hero")] + public string Hero { get; set; } + + [JsonProperty("Core")] + public CardSpec[] Core { get; set; } + + [JsonProperty("Variables")] + public CardSpec[] Variables { get; set; } + } + + public partial class CardSpec + { + [JsonProperty("Name")] + public string Name { get; set; } + + [JsonProperty("IsPitchless")] + public bool IsPitchless { get; set; } + + [JsonProperty("Count", NullValueHandling = NullValueHandling.Ignore)] + public string Count { get; set; } + + [JsonProperty("R", NullValueHandling = NullValueHandling.Ignore)] + public string R { get; set; } + + [JsonProperty("Y", NullValueHandling = NullValueHandling.Ignore)] + public string Y { get; set; } + + [JsonProperty("B", NullValueHandling = NullValueHandling.Ignore)] + public string B { get; set; } + } + + public class DeckGenerator + { + static IEnumerable> GetPermutations(IEnumerable list, int length) + { + if (length == 1) + return list.Select(t => new T[] { t }); + return GetPermutations(list, length - 1).SelectMany(t => list.Where(o => !t.Contains(o)), (t1, t2) => t1.Concat(new T[] { t2 })); + } + + static IEnumerable> GetPermutationsWithRept(IEnumerable list, int length) + { + if (length == 1) + return list.Select(t => new T[] { t }); + return GetPermutationsWithRept(list, length - 1).SelectMany(t => list, (t1, t2) => t1.Concat(new T[] { t2 })); + } + + static IEnumerable> GetKCombsWithRept(IEnumerable list, int length) where T : IComparable + { + if (length == 1) return list.Select(t => new T[] { t }); + return GetKCombsWithRept(list, length - 1) + .SelectMany(t => list.Where(o => o.CompareTo(t.Last()) >= 0), + (t1, t2) => t1.Concat(new T[] { t2 })); + } + + static IEnumerable> CartesianProduct (IEnumerable> sequences) + { + IEnumerable> emptyProduct = + new[] { Enumerable.Empty() }; + return sequences.Aggregate( + emptyProduct, + (accumulator, sequence) => + from accseq in accumulator + from item in sequence + select accseq.Concat(new[] {item})); + } + + private List? DeckConfigs = new List(); + public List> DeckLists = new List>(); + + private readonly int REASONABLE_MAX = 10; + + private bool IsAssortedThree(string pValue) { return pValue == "3*";} + private bool IsAssortedTwo(string pValue) { return pValue == "2*";} + private bool IsAssortedOne(string pValue) { return pValue == "1*";} + private bool ProcessCountString(string count, ref int rAssorted3Count, ref int rAssorted2Count, ref int rAssorted1Count) + { + if ( count == "" ) + return false; + + if ( IsAssortedThree(count) ) + { + rAssorted3Count++; + return true; + } + + if ( IsAssortedTwo(count) ) + { + rAssorted2Count++; + return true; + } + + if ( IsAssortedOne(count) ) + { + rAssorted1Count++; + return true; + } + + return false; + } + public void LoadDeckConfigs(string pfilePath) + { + using (StreamReader file = File.OpenText(pfilePath)) + { + DeckConfigs = JsonConvert.DeserializeObject > (file.ReadToEnd()); + } + + var three_over_two = GetPermutationsWithRept(new[] {1, 2}, 2).Select(c => c.ToList()).ToList(); + three_over_two.RemoveAll(i => i.Sum() != 3); + var three_over_three = GetPermutationsWithRept(new[] {0, 1, 2}, 3).Select(c => c.ToList()).ToList(); + three_over_three.RemoveAll(i => i.Sum() != 3); + + var two_over_two = GetPermutationsWithRept(new[] {0, 1, 2}, 2).Select(c => c.ToList()).ToList(); + two_over_two.RemoveAll(i => i.Sum() != 2); + var two_over_three = GetPermutationsWithRept(new[] {0, 1, 2}, 3).Select(c => c.ToList()).ToList(); + two_over_three.RemoveAll(i => i.Sum() != 2); + + var one_over_two = GetPermutationsWithRept(new[] {0, 1}, 2).Select(c => c.ToList()).ToList(); + one_over_two.RemoveAll(i => i.Sum() != 1); + var one_over_three = GetPermutationsWithRept(new[] {0, 1}, 3).Select(c => c.ToList()).ToList(); + one_over_three.RemoveAll(i => i.Sum() != 1); + + Debug.Assert(DeckConfigs != null, nameof(DeckConfigs) + " != null"); + foreach ( var config in DeckConfigs ) + { + List>> variable_card_perm_lists = new List>>(); + foreach ( var variable_card in config.Variables ) + { + int assorted_3_count = 0; + int assorted_2_count = 0; + int assorted_1_count = 0; + + bool spans_red = ProcessCountString(variable_card.R, ref assorted_3_count, ref assorted_2_count, ref assorted_1_count); + bool spans_blue = ProcessCountString(variable_card.B, ref assorted_3_count, ref assorted_2_count, ref assorted_1_count); + + ProcessCountString(variable_card.Y, ref assorted_3_count, ref assorted_2_count, ref assorted_1_count); + + List> counts = null; + if ( assorted_3_count == 2 ) + counts = three_over_two.Select(x => x.ToList()).ToList(); + else if (assorted_3_count == 3) + counts = three_over_three.Select(x => x.ToList()).ToList(); + else if (assorted_2_count == 2) + counts = two_over_two.Select(x => x.ToList()).ToList(); + else if (assorted_2_count == 3) + counts = two_over_three.Select(x => x.ToList()).ToList(); + else if (assorted_1_count == 2) + counts = one_over_two.Select(x => x.ToList()).ToList(); + else if (assorted_1_count == 3) + counts = one_over_three.Select(x => x.ToList()).ToList(); + + if ( (!spans_red || !spans_blue) && counts != null ) + { + for ( int i = 0; i < counts.Count; i++ ) + { + if ( spans_red ) + counts[i].Add(int.Parse(variable_card.B)); + if (spans_blue) + counts[i].Insert(0,int.Parse(variable_card.R)); + } + } + variable_card_perm_lists.Add(counts); + } + + var lists = CartesianProduct(variable_card_perm_lists).ToList(); + Console.WriteLine(lists.Count); + + foreach ( var variation in lists ) + { + var card_list = new Dictionary(); + + foreach ( var card in config.Core ) + { + if ( card.IsPitchless ) + { + card_list.Add(card.Name, int.Parse(card.Count)); + } + else + { + card_list.Add(card.Name + " - R", int.Parse(card.R)); + card_list.Add(card.Name + " - Y", int.Parse(card.Y)); + card_list.Add(card.Name + " - B", int.Parse(card.B)); + } + } + + var variation_counts = variation.ToList(); + for ( int i = 0; i < config.Variables.Length; i++ ) + { + var card = config.Variables[i]; + var counts = variation_counts[i]; + if(counts[0] > 0) + card_list.Add(card.Name + " - R", counts[0]); + if(counts[1] > 0) + card_list.Add(card.Name + " - Y", counts[1]); + if(counts[2] > 0) + card_list.Add(card.Name + " - B", counts[2]); + } + + int card_count = card_list.Sum(i => i.Value); + if ( card_count != 45 ) + { + Console.WriteLine(card_list.ToString()); + Debug.Assert(card_count == 45); + } + + DeckLists.Add(card_list); + } + } + } + + public void DetermineMaxBuilds(in Dictionary pCollectionCounts) + { + var timer = new Stopwatch(); + timer.Start(); + + // Limit this to per hero once i have more specs written + var range = Enumerable.Range(0, DeckLists.Count); + List largest_buildable_combo = new List(); + for (int i = 1; i <= REASONABLE_MAX; i++) + { + bool combo_was_built = false; + var combos = GetKCombsWithRept(range, i).Select(c => c.ToList()); + var ints = pCollectionCounts; + + Parallel.ForEach(combos, (combo, state) => + { + var temp_card_counts = new Dictionary(ints); + if (ComboIsBuildable(combo, temp_card_counts)) + { + // Cache it off somehwere as the largest buildable combo + largest_buildable_combo = new List(combo); + // Move on and enlarge the number of decks we are trying to build + combo_was_built = true; + state.Break(); + } + }); + + /* + foreach (var combo in combos) + { + // Multithread! + var temp_card_counts = new Dictionary(pCollectionCounts); + if (ComboIsBuildable(combo, temp_card_counts)) + { + // Cache it off somehwere as the largest buildable combo + largest_buildable_combo = new List(combo); + // Move on and enlarge the number of decks we are trying to build + combo_was_built = true; + break; + } + } + */ + + if (!combo_was_built) + break; + } + Console.WriteLine(string.Join(",", largest_buildable_combo)); + Dictionary totals_for_combo_build = new Dictionary(); + foreach (var index in largest_buildable_combo) + { + var deck = DeckLists[index]; + foreach (var entry in deck) + { + if (!totals_for_combo_build.ContainsKey(entry.Key)) + totals_for_combo_build[entry.Key] = 0; + totals_for_combo_build[entry.Key] += entry.Value; + } + var f = Newtonsoft.Json.JsonConvert.SerializeObject(deck, Newtonsoft.Json.Formatting.Indented); + Console.WriteLine(f); + } + var total_counts = Newtonsoft.Json.JsonConvert.SerializeObject(totals_for_combo_build, Newtonsoft.Json.Formatting.Indented); + Console.WriteLine(total_counts); + timer.Stop(); + TimeSpan timeTaken = timer.Elapsed; + Console.WriteLine("Time taken: " + timeTaken.ToString(@"m\:ss\.fff")); + } + + private bool DeckIsBuildable(in Dictionary pDeckCardCounts, in Dictionary pCardCounts) + { + foreach (var required_count in pDeckCardCounts) + { + if (pCardCounts[required_count.Key] - required_count.Value < 0) + return false; + } + + return true; + } + private bool ComboIsBuildable(in List pCombination, Dictionary pCardCounts) + { + Console.WriteLine("({1})Testing combo: {0}", String.Join(",", pCombination), Thread.CurrentThread.ManagedThreadId); + foreach (var index in pCombination) + { + var deck = DeckLists[index]; + if (!DeckIsBuildable(deck, pCardCounts)) + return false; + + foreach (var required_count in deck) + pCardCounts[required_count.Key] -= required_count.Value; + } + return true; + } + } +} diff --git a/FABStarterDeckGen/FABCollectionReader.cs b/FABStarterDeckGen/FABCollectionReader.cs new file mode 100644 index 0000000..7fb72d8 --- /dev/null +++ b/FABStarterDeckGen/FABCollectionReader.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using IronXL; +using static System.Text.RegularExpressions.Regex; + +namespace FabStarterDeckGen +{ + public class FABCollectionReader + { + public static readonly string COLUMN_NUMBER = "B"; + public static readonly string COLUMN_NAME = "C"; + public static readonly string COLUMN_RARITY = "D"; + public static readonly string COLUMN_RED = "E"; + public static readonly string COLUMN_YELLOW = "F"; + public static readonly string COLUMN_BLUE = "G"; + + public static readonly string[] COLUMNS_PITCHES = { COLUMN_RED, COLUMN_YELLOW, COLUMN_BLUE }; + public static readonly string[] COLUMNS_DETAILS = { COLUMN_NUMBER, COLUMN_NAME, COLUMN_RARITY }; + + public readonly int STARTING_ROW_INDEX = 3; + + public Dictionary CollectionCounts = new Dictionary(); + public void LoadCollection(string pFilePath, ReferenceCardLoader pReferenceCard) + { + WorkBook workbook = WorkBook.Load(pFilePath); + + foreach (var sheet in workbook.WorkSheets) + { + var current_row_index = STARTING_ROW_INDEX; + var current_card_name = sheet[COLUMN_NAME + current_row_index.ToString()].StringValue; + while (current_card_name != "") + { + Console.WriteLine(current_card_name); + var reference_cards = pReferenceCard.GetCardsWithName(current_card_name); + + foreach (var card in reference_cards) + { + if (card.IsPitchless) + { + if( !CollectionCounts.ContainsKey(card.CleanName) ) + CollectionCounts.Add(card.CleanName, 0); + + CollectionCounts[card.CleanName] += sheet[COLUMN_RED + current_row_index.ToString()].IntValue; + } + else + { + if (card.IsRed) + { + if( !CollectionCounts.ContainsKey(card.CleanNameRed) ) + CollectionCounts.Add(card.CleanNameRed, 0); + + CollectionCounts[card.CleanNameRed] += sheet[COLUMN_RED + current_row_index.ToString()].IntValue; + } + + if (card.IsYellow) + { + if( !CollectionCounts.ContainsKey(card.CleanNameYellow) ) + CollectionCounts.Add(card.CleanNameYellow, 0); + + CollectionCounts[card.CleanNameYellow] += sheet[COLUMN_YELLOW + current_row_index.ToString()].IntValue; + } + + if (card.IsBlue) + { + if( !CollectionCounts.ContainsKey(card.CleanNameBlue) ) + CollectionCounts.Add(card.CleanNameBlue, 0); + + CollectionCounts[card.CleanNameBlue] += sheet[COLUMN_BLUE + current_row_index.ToString()].IntValue; + } + } + } + + current_row_index++; + current_card_name = sheet[COLUMN_NAME + current_row_index.ToString()].StringValue; + } + } + } + } +} \ No newline at end of file diff --git a/FABStarterDeckGen/FabStarterDeckGen.csproj b/FABStarterDeckGen/FabStarterDeckGen.csproj new file mode 100644 index 0000000..c09b4fc --- /dev/null +++ b/FABStarterDeckGen/FabStarterDeckGen.csproj @@ -0,0 +1,24 @@ + + + + Exe + net5.0 + enable + enable + + + + DEBUG;TRACE + + + + + + + + + + + + + diff --git a/FABStarterDeckGen/Program.cs b/FABStarterDeckGen/Program.cs new file mode 100644 index 0000000..695631b --- /dev/null +++ b/FABStarterDeckGen/Program.cs @@ -0,0 +1,18 @@ +// See https://aka.ms/new-console-template for more information + +using System; +using System.Collections.Generic; +using FabStarterDeckGen; +using Newtonsoft.Json; + +string DATA_DIR = @"D:\Dev_P\FabCollectionTracker"; +var ref_card_loader = new ReferenceCardLoader(); +ref_card_loader.LoadSetCSV(DATA_DIR + @"\csvs\set.csv"); +ref_card_loader.LoadCardCSV(DATA_DIR + @"\csvs\card.csv"); + +var collection_reader = new FABCollectionReader(); +collection_reader.LoadCollection(DATA_DIR + @"\FabCollection-Generated.xlsx", ref_card_loader); + +var deckGenerator = new DeckGenerator(); +deckGenerator.LoadDeckConfigs(DATA_DIR + @"\DeckLists.json"); +deckGenerator.DetermineMaxBuilds(collection_reader.CollectionCounts); \ No newline at end of file diff --git a/FABStarterDeckGen/ReferenceCardLoader.cs b/FABStarterDeckGen/ReferenceCardLoader.cs new file mode 100644 index 0000000..6347a0e --- /dev/null +++ b/FABStarterDeckGen/ReferenceCardLoader.cs @@ -0,0 +1,178 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using CsvHelper; +using CsvHelper.Configuration; + +namespace FabStarterDeckGen +{ + public class ReferenceCardRow + { + public string Identifiers; + public string Set_Identifiers; + public string Name; + public string Pitch; + public string Rarity; + } + + public class ReferenceCardRowMap : ClassMap + { + public ReferenceCardRowMap() + { + Map(m => m.Identifiers).Name("Identifiers"); + Map(m => m.Set_Identifiers).Name("Set Identifiers"); + Map(m => m.Name).Name("Name"); + Map(m => m.Pitch).Name("Pitch"); + Map(m => m.Rarity).Name("Rarity"); + } + } + + public class CardInstance + { + private Regex clean_card_name_regex = new Regex(@"[^a-zA-Z0-9_ ]+"); + public CardInstance(ReferenceCardRow pBase) + { + Name = pBase.Name; + Pitch = pBase.Pitch; + Rarity = pBase.Rarity; + foreach (var part in pBase.Identifiers.Split(',')) + { + if (part == "null") + continue; + Identifiers.Add(part); + } + + foreach (var part in pBase.Set_Identifiers.Split(',')) + { + if (part == "null") + continue; + Set_Identifiers.Add(part); + } + + CleanName = clean_card_name_regex.Replace(Name, ""); + + } + public List Identifiers = new List(); + public List Set_Identifiers = new List(); + public string Name; + public string CleanName; + public string Pitch; + public string Rarity; + + public bool IsPitchless => Pitch == ""; + public bool IsRed => Pitch == "1"; + public bool IsYellow => Pitch == "2"; + public bool IsBlue => Pitch == "3"; + + public string CleanNameRed => CleanName + " - R"; + public string CleanNameYellow => CleanName + " - Y"; + public string CleanNameBlue => CleanName + " - B"; + } + + public class ReferenceSetRow + { + public string Identifier; + public string Name; + public string Initial_Release_Dates; + } + + public class ReferenceSetRowMap : ClassMap + { + public ReferenceSetRowMap() + { + Map(m => m.Identifier).Name("Identifier"); + Map(m => m.Initial_Release_Dates).Name("Initial Release Dates"); + Map(m => m.Name).Name("Name"); + } + } + + public class SetInstance + { + public SetInstance(ReferenceSetRow pBase) + { + Identifier = pBase.Identifier; + Name = pBase.Name; + var release_dates = new List(); + foreach (var part in pBase.Initial_Release_Dates.Split(',')) + { + if (part == "null") + continue; + release_dates.Add(DateTime.Parse(part)); + } + + if (release_dates.Count > 0) + { + release_dates.Sort(); + EarliestReleaseDate = release_dates[0]; + } + else + { + EarliestReleaseDate = DateTime.UnixEpoch; + } + } + + public string Identifier; + public string Name; + public DateTime EarliestReleaseDate; + public bool IsPromoSet; + public bool IsDeckSet; + public bool IsClassicBattleSet; + } + + public class ReferenceCardLoader + { + private List ReferenceSets; + private List ReferenceCards; + + public List Sets = new List(); + public List Cards = new List(); + + public void LoadSetCSV(string pFilePath) + { + var config = new CsvConfiguration(CultureInfo.CurrentCulture) { Delimiter = "\t", Encoding = Encoding.UTF8 }; + using (var reader = new StreamReader(pFilePath)) + using (var csv = new CsvReader(reader, config)) + { + csv.Context.RegisterClassMap(); + ReferenceSets = csv.GetRecords().ToList(); + } + + foreach (var set in ReferenceSets) + { + Sets.Add(new SetInstance(set)); + } + } + + public void LoadCardCSV(string pFilePath) + { + var config = new CsvConfiguration(CultureInfo.CurrentCulture) { Delimiter = "\t", Encoding = Encoding.UTF8 }; + using (var reader = new StreamReader(pFilePath)) + using (var csv = new CsvReader(reader, config)) + { + csv.Context.RegisterClassMap(); + ReferenceCards = csv.GetRecords().ToList(); + } + + foreach (var card in ReferenceCards) + { + Cards.Add(new CardInstance(card)); + } + } + + public List GetCardsWithName(string pName) + { + List card_list = new List(); + foreach (var card in Cards) + { + if(card.Name != pName) + continue; + card_list.Add(card); + } + return card_list; + } + } +} \ No newline at end of file diff --git a/FabStarterDeckGen.sln b/FabStarterDeckGen.sln new file mode 100644 index 0000000..1492a6a --- /dev/null +++ b/FabStarterDeckGen.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FabStarterDeckGen", ".\FabStarterDeckGen\FabStarterDeckGen.csproj", "{FEDDE3A6-84BD-4782-971A-BFFDC6A75C1C}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {FEDDE3A6-84BD-4782-971A-BFFDC6A75C1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FEDDE3A6-84BD-4782-971A-BFFDC6A75C1C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FEDDE3A6-84BD-4782-971A-BFFDC6A75C1C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FEDDE3A6-84BD-4782-971A-BFFDC6A75C1C}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal