added support for multiple copies of cards, simplified sorting

This commit is contained in:
Lumen Keyes 2024-08-29 13:10:59 -06:00
parent 038dd4d78a
commit 238a4017f4
3 changed files with 953 additions and 738 deletions

View File

@ -0,0 +1,100 @@
1 Ancestral Recall (VMA) 1
1 Arid Mesa (MH2) 436 *E*
1 Boseiju, Who Endures (NEO) 266
1 Botanical Sanctum (KLD) 244
1 Brainstorm (MMQ) 61
1 Breeding Pool (RNA) 246
1 Consider (MID) 44
1 Cosmic Rebirth (MAT) 78 *F*
1 Counterspell (MMQ) 69
1 Cryptic Command (MMA) 38
1 Deserted Beach (WHO) 270
1 Dovin's Veto (TSR) 375
1 Dreamroot Cascade (VOW) 262
1 Dress Down (MH2) 39
1 Eiganjo, Seat of the Empire (NEO) 268
1 Entreat the Angels (AVR) 20
1 Field of the Dead (M20) 247
1 Flooded Strand (MH3) 220
1 Force of Negation (H1R) 9 *F*
1 Force of Will (ME1) 33
1 Get Lost (LCI) 333
1 Gideon Jura (ROE) 21
1 Gitaxian Probe (NPH) 35
1 Growth Spiral (RNA) 178
1 Hallowed Fountain (RTR) 241
1 Hedge Maze (MKM) 326 *F*
1 Ice-Fang Coatl (H1R) 27 *F*
1 Island (MMQ) 335 *F*
1 Jace, the Mind Sculptor (EMA) 57
1 Karakas (TD0) B32
1 Leyline Binding (PRM) 103396
1 Library of Alexandria (ARN) 76
1 Logic Knot (FUT) 52
1 Lose Focus (MH2) 49
1 Lush Portico (MKM) 327
1 Mana Leak (PLST) DDN-64
1 Marsh Flats (MH2) 437 *E*
1 Mental Misstep (NPH) 38
1 Mental Note (JUD) 46
1 Merchant Scroll (8ED) 91
1 Meticulous Archive (MKM) 328 *F*
1 Misty Rainforest (MH2) 250
1 Murktide Regent (MH2) 337 *F*
1 Mystic Sanctuary (ELD) 247
1 Mystical Tutor (MIR) 80
1 No More Lies (MKM) 221
1 Oko, Thief of Crowns (ELD) 197
1 Opt (STA) 19
1 Otawara, Soaring City (NEO) 271
1 Path to Exile (PF20) 1 *F*
1 Plains (INV) 331 *F*
1 Planar Genesis (MH3) 198
1 Polluted Delta (MH3) 224
1 Ponder (C21) 125
1 Portent (ICE) 90
1 Preordain (CMR) 84
1 Prismatic Ending (MH2) 384
1 Prismatic Vista (H1R) 40 *E*
1 Remand (RVR) 59
1 Reprieve (LTR) 26
1 Savannah (LEA) 280
1 Scalding Tarn (ZNE) 7
1 Scapeshift (M19) 201
1 Seachrome Coast (ONE) 258
1 Seasoned Dungeoneer (CLB) 610
1 Sensei's Divining Top (CHK) 268
1 Serum Visions (5DN) 36
1 Shark Typhoon (IKO) 67
1 Sleight of Hand (P02) 46
1 Snapcaster Mage (ISD) 78
1 Snow-Covered Forest (KHM) 285
1 Snow-Covered Island (ICE) 371
1 Snow-Covered Plains (ICE) 367
1 Solitude (MH2) 307 *F*
1 Spara's Headquarters (SNC) 257
1 Spell Pierce (SLD) 41
1 Spell Snare (DIS) 33
1 Supreme Verdict (RTR) 201
1 Swords to Plowshares (ICE) 54
1 Tamiyo, Inquisitive Student // Tamiyo, Seasoned Scholar (MH3) 443
1 Teferi, Hero of Dominaria (DOM) 207
1 Teferi, Time Raveler (WAR) 221
1 Temple Garden (RAV) 284
1 Terminus (AVR) 38
1 The One Ring (LTR) 451
1 Thought Scour (2X2) 351
1 Treasure Cruise (TSR) 319
1 Triumph of Saint Katherine (40K) 17
1 Tropical Island (LEA) 283
1 Tundra (LEA) 284
1 Underground Sea (SUM) 290
1 Up the Beanstalk (WOE) 195
1 Uro, Titan of Nature's Wrath (PTHB) 229p
1 Verdant Catacombs (MH2) 440 *E*
1 Wall of Blossoms (STH) 125
1 Wall of Omens (2X2) 344
1 Wasteland (SLD) 178
1 White Plume Adventurer (CLB) 558 *F*
1 Windswept Heath (KTK) 248
10 Plains (ONS) 330

1465
output.pdf

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,7 @@
//TODO: consider eliminating the TextCard struct
//TODO: add some kind of "update" command to support pulling new oracle data //TODO: add some kind of "update" command to support pulling new oracle data
//TODO: implement oracleFileName as a cli arg //TODO: implement oracleFileName as a cli arg
//TODO: add more robust tests //TODO: add more robust tests
//TODO: support other list formats //TODO: support other list formats
//TODO: support multiple copies of cards
const std = @import("std"); const std = @import("std");
// https://github.com/Hejsil/zig-clap // https://github.com/Hejsil/zig-clap
@ -12,7 +10,6 @@ const clap = @import("clap");
// https://codeberg.org/dude_the_builder/zg // https://codeberg.org/dude_the_builder/zg
const DisplayWidth = @import("DisplayWidth"); const DisplayWidth = @import("DisplayWidth");
// https://github.com/AndreRenaud/PDFGen // https://github.com/AndreRenaud/PDFGen
// including the header file only works for a full build, "zig run" can't find it // including the header file only works for a full build, "zig run" can't find it
const c = @cImport({ const c = @cImport({
@ -38,20 +35,14 @@ const Card = struct {
toughness: ?[]const u8 = null, //coerced to string toughness: ?[]const u8 = null, //coerced to string
card_faces: ?[]Card = null, //array of cards card_faces: ?[]Card = null, //array of cards
loyalty: ?[]const u8 = null, //coerced to string loyalty: ?[]const u8 = null, //coerced to string
isFace: bool = undefined, //cheeky little property that I added object: []const u8 = "", //string
allText: [][]const u8 = undefined//
}; };
const TextCard = struct { fn compareCards(ctx: void, lhs: Card, rhs: Card) bool {
lines: [][]const u8 = undefined, _ = ctx;
}; return lhs.allText.len < rhs.allText.len;
}
const CardSortContext = struct {
const Self = @This();
list: []TextCard,
pub fn lessThan(ctx: Self, a: usize, b: usize) bool {
return ctx.list[a].lines.len < ctx.list[b].lines.len;
}
};
//dimension constants and defaults //dimension constants and defaults
const cardWidth = 30; const cardWidth = 30;
@ -104,43 +95,47 @@ pub fn main() !void {
constantHeight = std.mem.eql(u8, choice, "true"); constantHeight = std.mem.eql(u8, choice, "true");
} }
// var timer = try std.time.Timer.start();
const oracleFileSize = (try cwd.statFile(oracleFileName)).size; const oracleFileSize = (try cwd.statFile(oracleFileName)).size;
const oracleFile = try cwd.readFileAlloc(allocator, oracleFileName, oracleFileSize); const oracleFile = try cwd.readFileAlloc(allocator, oracleFileName, oracleFileSize);
const parsedJson = try json.parseFromSlice([]Card, allocator, oracleFile, .{ .ignore_unknown_fields = true }); const parsedJson = try json.parseFromSlice([]Card, allocator, oracleFile, .{ .ignore_unknown_fields = true });
var cardNames = std.BufSet.init(allocator); var cards = std.ArrayList(Card).init(allocator);
const listFileSize = (try cwd.statFile(listFileName)).size; const listFileSize = (try cwd.statFile(listFileName)).size;
const listText = try cwd.readFileAlloc(allocator, listFileName, listFileSize); const listText = try cwd.readFileAlloc(allocator, listFileName, listFileSize);
var listLines = std.mem.splitAny(u8, listText, "\n"); var listLines = std.mem.splitAny(u8, listText, "\n");
while (listLines.next()) |line| { while (listLines.next()) |line| {
if (line.len < 5) break; if (line.len < 5) break;
const cardName = line[indexOf(u8, line, " ").? + 1 .. indexOf(u8, line, "(").? - 1]; const firstSpace = indexOf(u8, line, " ").?;
try cardNames.insert(cardName); const cardName = line[firstSpace + 1 .. indexOf(u8, line, "(").? - 1];
const countString = line[0..firstSpace];
const printCount = try std.fmt.parseInt(u8, countString, 10);
for(0..printCount) |_| {
try cards.append(Card{.name = cardName});
}
} }
var cards = std.StringArrayHashMap(TextCard).init(allocator);
for (parsedJson.value) |*cardObj| { for (parsedJson.value) |*cardObj| {
if (cardNames.contains(cardObj.name)) { for(cards.items) |*cardInfo| {
cardObj.isFace = false; if (std.mem.eql(u8, cardInfo.name, cardObj.name)) {
const printableCard = try card(cardObj.*, allocator); cardInfo.allText = try card(cardObj.*, allocator);
try cards.put(cardObj.name, printableCard);
} }
}
} }
cards.sort(CardSortContext{ .list = cards.values() }); std.mem.sort(Card, cards.items, {}, compareCards);
var allPrinted = std.ArrayList([]const u8).init(allocator); var allPrinted = std.ArrayList([]const u8).init(allocator);
const pdf_doc: *c.pdf_doc = @ptrCast(c.pdf_create(pdfWidth, pdfHeight, &c.pdf_info{ .creator = ("My Software" ++ " " ** 53).* }).?); const pdf_doc: *c.pdf_doc = @ptrCast(c.pdf_create(pdfWidth, pdfHeight, &c.pdf_info{ .creator = ("My Software" ++ " " ** 53).* }).?);
var rowToPrint = std.ArrayList(TextCard).init(allocator); var rowToPrint = std.ArrayList(Card).init(allocator);
for (cards.values()) |cardText| { for (cards.items) |cardText| {
try rowToPrint.append(cardText); try rowToPrint.append(cardText);
if (rowToPrint.items.len >= 3) { if (rowToPrint.items.len >= 3) {
try cardRow.print(allocator, rowToPrint.items, &allPrinted); try printRow(allocator, rowToPrint.items[0..3].*, &allPrinted);
rowToPrint.clearAndFree(); rowToPrint.clearAndFree();
} }
} else { } else {
try cardRow.print(allocator, rowToPrint.items, &allPrinted); try printRow(allocator, [3]Card{rowToPrint.items[0], Card{}, Card{}}, &allPrinted);
rowToPrint.clearAndFree(); rowToPrint.clearAndFree();
} }
var page = c.pdf_append_page(pdf_doc); var page = c.pdf_append_page(pdf_doc);
@ -161,7 +156,8 @@ pub fn main() !void {
fn card( fn card(
cardObj: Card, cardObj: Card,
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
) !TextCard { ) ![][]const u8 {
const isFace= std.mem.eql(u8, cardObj.object, "card_face");
var cardText = std.ArrayList([]const u8).init(allocator); var cardText = std.ArrayList([]const u8).init(allocator);
var fullUnformattedText = std.ArrayList(u8).init(allocator); var fullUnformattedText = std.ArrayList(u8).init(allocator);
@ -176,10 +172,7 @@ fn card(
var finalOracleText: [30 * 100]u8 = undefined; var finalOracleText: [30 * 100]u8 = undefined;
_ = std.mem.replace(u8, oracleBuf.items, cardObj.name, "~", &finalOracleText); _ = std.mem.replace(u8, oracleBuf.items, cardObj.name, "~", &finalOracleText);
// print("{s}\n", .{finalOracleText}); if (cardObj.card_faces == null or isFace) {
if (cardObj.card_faces == null or cardObj.isFace) {
try fullUnformattedText.appendSlice(try std.mem.concat(allocator, u8, &[_][]const u8{ try fullUnformattedText.appendSlice(try std.mem.concat(allocator, u8, &[_][]const u8{
cardObj.name, cardObj.name,
if (cardObj.mana_cost.len > 0) " " else "", if (cardObj.mana_cost.len > 0) " " else "",
@ -189,20 +182,17 @@ fn card(
if(cardObj.power) |_| try std.fmt.allocPrint(allocator, "({s}/{s})", .{cardObj.power.?, cardObj.toughness.?}) else "", if(cardObj.power) |_| try std.fmt.allocPrint(allocator, "({s}/{s})", .{cardObj.power.?, cardObj.toughness.?}) else "",
if(cardObj.loyalty) |_| try std.fmt.allocPrint(allocator, "[{s}]", .{cardObj.loyalty.?}) else "" if(cardObj.loyalty) |_| try std.fmt.allocPrint(allocator, "[{s}]", .{cardObj.loyalty.?}) else ""
})); }));
} }
if (cardObj.card_faces) |faces| { if (cardObj.card_faces) |faces| {
for (faces, 0..) |*face, idx| { for (faces, 0..) |*face, idx| {
face.isFace = true; const faceText = try card(face.*, allocator);
const faceText = (try card(face.*, allocator)).lines;
try fullUnformattedText.appendSlice(std.mem.trim(u8, try std.mem.join(allocator, " ", faceText), "\n")); try fullUnformattedText.appendSlice(std.mem.trim(u8, try std.mem.join(allocator, " ", faceText), "\n"));
if (idx == 0) try fullUnformattedText.appendSlice("// "); if (idx == 0) try fullUnformattedText.appendSlice("// ");
} }
} }
var line = std.ArrayList(u8).init(allocator); var line = std.ArrayList(u8).init(allocator);
//split on spaces, newlines and null
var wordIterator = std.mem.splitAny(u8, fullUnformattedText.items, &.{'\n',' ',170}); var wordIterator = std.mem.splitAny(u8, fullUnformattedText.items, &.{'\n',' ',170});
while (wordIterator.next()) |word| { while (wordIterator.next()) |word| {
if (line.items.len + word.len + 1 >= cardWidth) { if (line.items.len + word.len + 1 >= cardWidth) {
@ -212,57 +202,49 @@ fn card(
if(word.len > 0) { if(word.len > 0) {
try line.appendSlice(word); try line.appendSlice(word);
try line.append(' '); try line.append(' ');
// print("{any}\n", .{word});
// print("{s}\n", .{line.items});
assert(line.items.len < 30); assert(line.items.len < 30);
} }
} else { } else {
try cardText.append(try line.toOwnedSlice()); try cardText.append(try line.toOwnedSlice());
} }
if(!cardObj.isFace and constantHeight) { if(!isFace and constantHeight) {
assert(cardText.items.len <= cardHeight); assert(cardText.items.len <= cardHeight);
try cardText.appendNTimes(" " ** (cardWidth - 2), cardHeight - cardText.items.len); try cardText.appendNTimes(" " ** (cardWidth - 2), cardHeight - cardText.items.len);
} else if(!cardObj.isFace) { } else if(!isFace) {
try cardText.append(" " ** (cardWidth - 2)); try cardText.append(" " ** (cardWidth - 2));
} }
assert(cardText.items.len <= cardHeight); assert(cardText.items.len <= cardHeight);
return TextCard{ .lines = try cardText.toOwnedSlice() }; return try cardText.toOwnedSlice();
} }
const linesList = std.MultiArrayList(cardRow);
fn optionalAccess(slice: anytype, index: usize) []const u8 { fn optionalAccess(slice: anytype, index: usize) []const u8 {
if(slice.len > index) return slice[index]; if(slice.len > index) return slice[index];
return ""; return "";
} }
const cardRow = struct {
fn print(allocator: std.mem.Allocator, cards: []TextCard, allPrinted: *std.ArrayList([]const u8)) !void {
var lines = std.ArrayList([]const u8).init(allocator);
const endIndex = if(cards.len > 1) std.mem.max(u64, &.{ fn printRow(allocator: std.mem.Allocator, cards: [3]Card, allPrinted: *std.ArrayList([]const u8)) !void {
cards[0].lines.len, var lines = std.ArrayList([]const u8).init(allocator);
cards[1].lines.len,
cards[2].lines.len
}) else cards[0].lines.len - 1;
for(0..endIndex) |idx| { const endIndex = std.mem.max(u64, &.{
try lines.append( cards[0].allText.len,
if(cards.len > 1) cards[1].allText.len,
try std.fmt.allocPrint(allocator, lineFormatter ** 3 ++ "\n", .{ cards[2].allText.len
optionalAccess(cards[0].lines, idx), });
optionalAccess(cards[1].lines, idx),
optionalAccess(cards[2].lines, idx)
})
else cards[0].lines[idx]);
}
const rowHeight = lines.items.len; for(0..endIndex) |idx| {
while (pageHeight - (allPrinted.items.len % pageHeight) <= rowHeight) { try lines.append(
assert(rowHeight <= pageHeight); try std.fmt.allocPrint(allocator, lineFormatter ** 3 ++ "\n", .{
try allPrinted.append(fullWidthSpacer); optionalAccess(cards[0].allText, idx),
} optionalAccess(cards[1].allText, idx),
for (lines.items) |line| { optionalAccess(cards[2].allText, idx)
try allPrinted.append(line); })
} );
} }
};
const rowHeight = lines.items.len;
while (pageHeight - (allPrinted.items.len % pageHeight) <= rowHeight) {
assert(rowHeight <= pageHeight);
try allPrinted.append(fullWidthSpacer);
}
try allPrinted.appendSlice(lines.items);
}