Skip to content

Instantly share code, notes, and snippets.

@jlennox
Last active September 22, 2023 17:19
Show Gist options
  • Save jlennox/934ac37c8b265ee88456cadcc058b7bd to your computer and use it in GitHub Desktop.
Save jlennox/934ac37c8b265ee88456cadcc058b7bd to your computer and use it in GitHub Desktop.
using OfficeOpenXml;
using OfficeOpenXml.Style;
using System.Drawing;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
// Source code is licensed under MIT 2023 Joseph Lennox
// scryfall does not appear to post a license for their data.
// This code is a mess designed to quickly gen some info :)
namespace MagicSearch;
internal class Program
{
static void Main(string[] args)
{
// "Default cards" database from https://scryfall.com/docs/api/bulk-data
const string file = @"C:\Users\joe\Desktop\delete\default-cards-20230921090601.json";
var cards = JsonSerializer.Deserialize<Card[]>(File.OpenRead(file), new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
var woods = cards.Where(t => t.Name == "Tainted Wood").ToArray();
var lands = cards
.Where(t => !t.Reprint)
.Where(t => !t.Variation)
.Where(t => t.Legalities.PreModernAllowed)
.Where(t => t.IsLand)
.OrderBy(t => t.ReleasedAt)
.ThenBy(t => t.Name)
.ToArray();
ExcelPackage.LicenseContext = LicenseContext.NonCommercial;
using var package = new ExcelPackage();
var byColorSheet = package.Workbook.Worksheets.Add("By color");
var byColorWithColorlessSheet = package.Workbook.Worksheets.Add("By color (with C)");
var byTypeSheet = package.Workbook.Worksheets.Add("By type");
var byTypeWithColorlessSheet = package.Workbook.Worksheets.Add("By type (with C)");
var produceMultipleSingleMana = package.Workbook.Worksheets.Add("Add multiple single-colored mana");
var produceMultipleMana = package.Workbook.Worksheets.Add("Add multiple mana");
// var byDuelColorSheet = package.Workbook.Worksheets.Add("By duel colors");
var byColorIdentitySheet = package.Workbook.Worksheets.Add("By color identity");
var allLandsSheet = package.Workbook.Worksheets.Add("All lands");
var infoSheet = package.Workbook.Worksheets.Add("Info");
infoSheet.Cells[1, 1].Value = "Likely not up to date source code: https://gist.github.com/jlennox/934ac37c8b265ee88456cadcc058b7bd";
infoSheet.Cells[2, 1].Value = $"Last generate {DateTime.Now}";
infoSheet.Cells[3, 1].Value = $"Database used {Path.GetFileNameWithoutExtension(file)}";
infoSheet.Cells[4, 1].Value = $"Questions/comments/complaints: @fizzbinx on Discord";
foreach (var sheet in new[] {
byTypeSheet, byTypeWithColorlessSheet,
byColorSheet, byColorWithColorlessSheet,
produceMultipleSingleMana, produceMultipleMana,
// byDuelColorSheet,
byColorIdentitySheet,
allLandsSheet })
{
sheet.Column((int)Columns.Name).Width = 20;
foreach (var color in new[] {
Columns.ColorR,
Columns.ColorG,
Columns.ColorU,
Columns.ColorB,
Columns.ColorW,
Columns.ColorC
})
{
sheet.Column((int)color).Width = 3;
}
sheet.Column((int)Columns.Set).Width = 15;
sheet.Column((int)Columns.Price).Style.HorizontalAlignment = ExcelHorizontalAlignment.Right;
sheet.Column((int)Columns.Text).Width = 300;
}
var duels = lands
.Where(DuelCard.IsMatch)
.Select(t => new DuelCard(t))
.ToArray();
static void PrintByGrouping(
ExcelWorksheet sheet,
string description,
IEnumerable<IGrouping<string, DuelCard>> grouped,
Func<DuelCard, string[]>? getManas = null,
bool colorTitle = false)
{
var i = 1;
sheet.Row(1).Height = 25;
var titleCells = sheet.Cells[i, 1, i, (int)Columns.MAX];
titleCells.Merge = true;
titleCells.Value = description;
titleCells.Style.Font.Size = 15;
titleCells.Style.Font.Bold = true;
++i;
foreach (var group in grouped)
{
var titleCell = sheet.Cells[i, 1];
titleCell.Value = group.Key;
titleCell.Style.Font.Size = 13;
for (var j = 1; j < 5; ++j) sheet.Cells[i, j].Style.Font.Bold = true;
if (colorTitle) {
var card = group.First().Card;
card.AddColorsToWorksheet(sheet, i, card.ColorIdentity);
}
++i;
foreach (var card in group)
{
var manas = getManas == null ? card.Manas : getManas(card);
card.AddToWorksheet(sheet, i, manas);
++i;
}
++i;
++i;
}
}
PrintByGrouping(byTypeSheet, "By duel land type", duels.GroupBy(t => t.GetGroupingKey(false)));
PrintByGrouping(byTypeWithColorlessSheet, "By duel land type, including tapping for colorless", duels.GroupBy(t => t.GetGroupingKey(true)));
PrintByGrouping(byColorSheet, "By produced colors", duels.GroupBy(t => t.GetColorKey(true)).OrderByDescending(t => t.Key.Length));
PrintByGrouping(byColorWithColorlessSheet, "By produced colors, including colorless", duels.GroupBy(t => t.GetColorKey(false)).OrderByDescending(t => t.Key.Length));
PrintByGrouping(
byColorIdentitySheet,
"By \"color identity\"",
duels.GroupBy(t => t.GetColorIdentity()).OrderByDescending(t => t.Key.Length),
null,
true);
PrintByGrouping(produceMultipleSingleMana,
"All mono colored land that produce multiple mana",
lands.Where(DuelCard.IsMultiMono).Select(t => new DuelCard(t)).OrderBy(t => t.Manas.First()).GroupBy(t => ""));
PrintByGrouping(produceMultipleMana,
"All multi-colored land that produce multiple mana",
lands.Where(DuelCard.IsMulti).Select(t => new DuelCard(t)).OrderBy(t => t.Manas.First()).GroupBy(t => ""));
PrintByGrouping(allLandsSheet, "All lands, even non-mana producing", lands.Select(t => new DuelCard(t)).GroupBy(t => ""));
var allDuels = new List<string[]>();
var allColors = new[] { "R", "G", "B", "U", "W" };
foreach (var c1 in allColors) foreach (var c2 in allColors) {
if (c1 != c2)
{
allDuels.Add(new[] { c1, c2 }.OrderBy(t => t).ToArray());
}
}
// PrintByGrouping(byDuelColorSheet,
// "By each 2 color combination, even if producing more color",
// duels.GroupBy(
// t => string.Concat(allDuels.FirstOrDefault(cs => t.ContainsMana(cs), new string[] { }))
// ));
var x = duels.Where(t => t.Card.ColorIdentity.Length > 1).ToArray();
var fileInfo = new FileInfo(@"C:\users\joe\desktop\delete\PMDuelLands.xlsx");
package.SaveAs(fileInfo);
}
}
enum Columns : int
{
Name = 1,
ColorR,
ColorG,
ColorU,
ColorB,
ColorW,
ColorC,
AfterColor,
Set,
Price,
AfterPrice,
Text,
MAX
}
class DuelCard
{
public Card Card { get; }
public string SingleLineText { get; }
public bool Fetch { get; }
public bool IsPain { get; }
public bool TapsColorless { get; }
public bool EtbTapped { get; }
public bool DelayedUntap { get; }
public bool Tainted { get; }
public bool Sacrifice { get; }
public bool Converter { get; }
public bool Bounce { get; }
public string[] Manas { get; }
private static readonly Regex _isConverter = new(@"{\d}, {T}: Add {\w}(?: or )?(?:{\w})", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex _isFetch = new(@"Search your library for a (\w+) or (\w+) card", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex _isMultiMono = new(@"(?:({[BGRUW]})\1)|(?:{[BGRUW]} for each)|(?:Add (?:two)|(?:three)(?:four) mana)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex _isMulti = new(@"{([BGRUW\d])}{(?!\1)[BGRUW\d]}", RegexOptions.Compiled | RegexOptions.IgnoreCase);
public DuelCard(Card card)
{
Card = card;
SingleLineText = card.OracleText.ReplaceLineEndings(" ");
Fetch = _isFetch.IsMatch(SingleLineText);
IsPain = SingleLineText.Contains("damage", StringComparison.InvariantCultureIgnoreCase);
TapsColorless = SingleLineText.Contains("{T}: Add {C}", StringComparison.InvariantCultureIgnoreCase);
EtbTapped = SingleLineText.Contains("enters the battlefield tapped", StringComparison.InvariantCultureIgnoreCase);
DelayedUntap = SingleLineText.Contains("untap during your", StringComparison.InvariantCultureIgnoreCase);
Tainted = SingleLineText.Contains("Activate only if you control a Swamp", StringComparison.InvariantCultureIgnoreCase);
Sacrifice = SingleLineText.Contains("Sacrifice", StringComparison.InvariantCultureIgnoreCase);
Converter = _isConverter.IsMatch(SingleLineText);
Manas = card.ProducedMana;
if (SingleLineText.Contains("sacrifice it unless you return a non-Lair land", StringComparison.CurrentCultureIgnoreCase) ||
SingleLineText.Contains("return an "))
{
Bounce = true;
Sacrifice = false;
}
if (Fetch)
{
var landToMana = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase)
{
{ "Forest", "G" },
{ "Swamp", "B" },
{ "Island", "U" },
{ "Mountain", "R" },
{ "Plains", "W" },
};
var match = _isFetch.Match(SingleLineText);
Manas = match.Groups.Cast<Group>().Skip(1).Select(t => landToMana[t.Value]).ToArray();
}
}
public static bool IsMatch(Card card)
{
if (_isFetch.IsMatch(card.OracleText)) return true;
return card.ProducedMana != null && card.ProducedMana.Length > 1;
}
public static bool IsMultiMono(Card card)
{
return _isMultiMono.IsMatch(card.OracleText);
}
public static bool IsMulti(Card card)
{
return card.ProducedMana?.Length > 1 && _isMulti.IsMatch(card.OracleText);
}
public bool ContainsMana(string[] mana)
{
return mana.All(t => Manas.Contains(t));
}
public string GetGroupingKey(bool includeColorless)
{
var list = new List<string>();
if (Fetch) list.Add("Fetch");
if (IsPain) list.Add("Pain");
if (includeColorless && TapsColorless) list.Add("Taps Colorless");
if (EtbTapped) list.Add("ETB Tapped");
if (DelayedUntap) list.Add("Delayed Untap");
if (Tainted) list.Add("Tainted");
if (Bounce) list.Add("Bounce");
if (Sacrifice) list.Add("Sacrifice");
if (Converter) list.Add("Converter");
return list.Count == 0 ? "Misc" : string.Join(", ", list);
}
public string GetColorKey(bool includeColorless)
{
return string.Concat(Manas
.Where(t => !includeColorless || t != "C")
.OrderBy(t => t));
}
public string GetColorIdentity()
{
if (Card.ColorIdentity == null) return "None";
return string.Concat(Card.ColorIdentity.OrderBy(t => t));
}
public void AddToWorksheet(ExcelWorksheet sheet, int row, string[] colors)
{
Card.AddToWorksheet(sheet, row, colors);
}
}
struct ColorInfo
{
public string Mana;
public Color Color;
public Color ForegroundColor;
public ColorInfo(string mana, Color color, Color foregroundColor)
{
Mana = mana;
Color = color;
ForegroundColor = foregroundColor;
}
}
class Card
{
public string Name { get; set; }
[JsonPropertyName("oracle_text")]
public string OracleText { get; set; }
public bool Reprint { get; set; }
public bool Variation { get; set; }
public Legalities Legalities { get; set; }
[JsonPropertyName("type_line")]
public string TypeLine { get; set; }
[JsonPropertyName("set_name")]
public string SetName { get; set; }
[JsonPropertyName("released_at")]
public DateOnly ReleasedAt { get; set; }
[JsonPropertyName("color_identity")]
public string[] ColorIdentity { get; set; }
[JsonPropertyName("produced_mana")]
public string[] ProducedMana { get; set; }
public Dictionary<string, string> Prices { get; set; }
public bool IsLand => TypeLine.Contains("Land");
public void AddColorsToWorksheet(ExcelWorksheet sheet, int row, string[] producedMana)
{
var colorInfos = new Dictionary<Columns, ColorInfo>
{
{ Columns.ColorR, new ColorInfo("R", Color.Red, Color.Black) },
{ Columns.ColorG, new ColorInfo("G", Color.Green, Color.Black) },
{ Columns.ColorU, new ColorInfo("U", Color.Blue, Color.White) },
{ Columns.ColorB, new ColorInfo("B", Color.Black, Color.White) },
{ Columns.ColorW, new ColorInfo("W", Color.FromArgb(230, 230, 230), Color.Black) },
{ Columns.ColorC, new ColorInfo("C", Color.WhiteSmoke, Color.Black) },
};
ExcelRange Set(Columns col, string val)
{
var cell = sheet.Cells[row, (int)col];
cell.Value = val;
return cell;
}
void SetColor(Columns col)
{
var info = colorInfos[col];
if (producedMana == null || !producedMana.Contains(info.Mana)) return;
var cell = Set(col, info.Mana);
cell.Style.Fill.SetBackground(info.Color);
cell.Style.Font.Color.SetColor(info.ForegroundColor);
cell.Style.HorizontalAlignment = ExcelHorizontalAlignment.Center;
}
SetColor(Columns.ColorR);
SetColor(Columns.ColorG);
SetColor(Columns.ColorU);
SetColor(Columns.ColorB);
SetColor(Columns.ColorW);
SetColor(Columns.ColorC);
}
public void AddToWorksheet(ExcelWorksheet sheet, int row, string[] producedMana)
{
ExcelRange Set(Columns col, string val)
{
var cell = sheet.Cells[row, (int)col];
cell.Value = val;
return cell;
}
Set(Columns.Name, Name);
AddColorsToWorksheet(sheet, row, producedMana);
Set(Columns.Set, SetName);
Set(Columns.Price, "$" + Prices.GetValueOrDefault("usd", "?"));
Set(Columns.Text, OracleText.ReplaceLineEndings(" "));
}
}
class Legalities
{
public string OldSchool { get; set; }
public string PreModern { get; set; }
public bool OldSchoolAllowed => OldSchool == "legal";
public bool PreModernAllowed => PreModern == "legal";
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment