landlord-ts 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +111 -0
- package/data/all_cards.json.gz +0 -0
- package/dist/chunk-I6ETCO5F.js +803 -0
- package/dist/index.cjs +2487 -0
- package/dist/index.d.cts +504 -0
- package/dist/index.d.ts +504 -0
- package/dist/index.js +1681 -0
- package/dist/simulation-worker.cjs +208 -0
- package/dist/simulation-worker.d.cts +2 -0
- package/dist/simulation-worker.d.ts +2 -0
- package/dist/simulation-worker.js +33 -0
- package/package.json +102 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2487 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
ALL_CARDS: () => ALL_CARDS,
|
|
24
|
+
BasicLandType: () => BasicLandType,
|
|
25
|
+
COLOR_CONSTRAINED_THRESHOLD: () => COLOR_CONSTRAINED_THRESHOLD,
|
|
26
|
+
CardKind: () => CardKind,
|
|
27
|
+
DRAW_DEPENDENT_THRESHOLD: () => DRAW_DEPENDENT_THRESHOLD,
|
|
28
|
+
DeckcodeError: () => DeckcodeError,
|
|
29
|
+
PlayOrder: () => PlayOrder,
|
|
30
|
+
Rarity: () => Rarity,
|
|
31
|
+
RunValidationError: () => RunValidationError,
|
|
32
|
+
SPECIAL_LANDS: () => SPECIAL_LANDS,
|
|
33
|
+
asLondonMulligan: () => asLondonMulligan,
|
|
34
|
+
asNeverMulligan: () => asNeverMulligan,
|
|
35
|
+
autoTapByTurn: () => autoTapByTurn,
|
|
36
|
+
availableTurnForLand: () => availableTurnForLand,
|
|
37
|
+
basicLandTypesFromTypeLine: () => basicLandTypesFromTypeLine,
|
|
38
|
+
checkTypesFromOracleText: () => checkTypesFromOracleText,
|
|
39
|
+
createMulberry32: () => createMulberry32,
|
|
40
|
+
deckFlatten: () => deckFlatten,
|
|
41
|
+
deckFromCards: () => deckFromCards,
|
|
42
|
+
deckFromList: () => deckFromList,
|
|
43
|
+
decklist: () => decklist,
|
|
44
|
+
drawCmcAutoTap: () => drawCmcAutoTap,
|
|
45
|
+
etbClauseFromOracle: () => etbClauseFromOracle,
|
|
46
|
+
getAllCards: () => getAllCards,
|
|
47
|
+
handFromOpeningAndDraws: () => handFromOpeningAndDraws,
|
|
48
|
+
hashCardName: () => hashCardName,
|
|
49
|
+
isLand: () => isLand,
|
|
50
|
+
isLandKind: () => isLandKind,
|
|
51
|
+
landKindFromOracleText: () => landKindFromOracleText,
|
|
52
|
+
loadCardsFromGzipBytes: () => loadCardsFromGzipBytes,
|
|
53
|
+
londonNever: () => londonNever,
|
|
54
|
+
manaCostCmc: () => manaCostCmc,
|
|
55
|
+
manaCostFromRgbuwc: () => manaCostFromRgbuwc,
|
|
56
|
+
manaCostsFromStr: () => manaCostsFromStr,
|
|
57
|
+
observationsForCard: () => observationsForCard,
|
|
58
|
+
pMana: () => pMana,
|
|
59
|
+
pManaGivenCmc: () => pManaGivenCmc,
|
|
60
|
+
pPlay: () => pPlay,
|
|
61
|
+
parseDecklist: () => parseDecklist,
|
|
62
|
+
playCmcAutoTap: () => playCmcAutoTap,
|
|
63
|
+
run: () => run,
|
|
64
|
+
runAsync: () => runAsync,
|
|
65
|
+
scryfallCardToCard: () => scryfallCardToCard,
|
|
66
|
+
scryfallCardsToCards: () => scryfallCardsToCards,
|
|
67
|
+
setAllCards: () => setAllCards,
|
|
68
|
+
simulationFromConfig: () => simulationFromConfig,
|
|
69
|
+
simulationFromConfigAsync: () => simulationFromConfigAsync,
|
|
70
|
+
wilsonHalfWidth: () => wilsonHalfWidth
|
|
71
|
+
});
|
|
72
|
+
module.exports = __toCommonJS(index_exports);
|
|
73
|
+
|
|
74
|
+
// node_modules/tsup/assets/cjs_shims.js
|
|
75
|
+
var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
|
|
76
|
+
var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
77
|
+
|
|
78
|
+
// src/card/mana-cost.ts
|
|
79
|
+
var R_BITS = 1;
|
|
80
|
+
var G_BITS = 2;
|
|
81
|
+
var B_BITS = 4;
|
|
82
|
+
var U_BITS = 8;
|
|
83
|
+
var W_BITS = 16;
|
|
84
|
+
var C_BITS = 32;
|
|
85
|
+
function emptyManaCost() {
|
|
86
|
+
return { bits: 0, r: 0, w: 0, b: 0, u: 0, g: 0, c: 0 };
|
|
87
|
+
}
|
|
88
|
+
function manaCostFromRgbuwc(r, g, b, u, w, c) {
|
|
89
|
+
return {
|
|
90
|
+
bits: calculateSignature(r, g, b, u, w, c),
|
|
91
|
+
r,
|
|
92
|
+
w,
|
|
93
|
+
b,
|
|
94
|
+
u,
|
|
95
|
+
g,
|
|
96
|
+
c
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function manaCostCmc(cost) {
|
|
100
|
+
return cost.r + cost.w + cost.b + cost.u + cost.g + cost.c;
|
|
101
|
+
}
|
|
102
|
+
function updateManaCostBits(cost) {
|
|
103
|
+
return {
|
|
104
|
+
...cost,
|
|
105
|
+
bits: calculateSignature(cost.r, cost.g, cost.b, cost.u, cost.w, cost.c)
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function calculateSignature(r, g, b, u, w, c) {
|
|
109
|
+
return Math.min(1, r) << 0 & R_BITS | Math.min(1, g) << 1 & G_BITS | Math.min(1, b) << 2 & B_BITS | Math.min(1, u) << 3 & U_BITS | Math.min(1, w) << 4 & W_BITS | Math.min(1, c) << 5 & C_BITS;
|
|
110
|
+
}
|
|
111
|
+
function manaColorFromStr(color) {
|
|
112
|
+
switch (color.charAt(0)) {
|
|
113
|
+
case "B":
|
|
114
|
+
return 2 /* Black */;
|
|
115
|
+
case "U":
|
|
116
|
+
return 3 /* Blue */;
|
|
117
|
+
case "G":
|
|
118
|
+
return 1 /* Green */;
|
|
119
|
+
case "R":
|
|
120
|
+
return 0 /* Red */;
|
|
121
|
+
case "W":
|
|
122
|
+
return 4 /* White */;
|
|
123
|
+
default:
|
|
124
|
+
return 5 /* Colorless */;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function applyColor(cost, color, count) {
|
|
128
|
+
switch (color) {
|
|
129
|
+
case 2 /* Black */:
|
|
130
|
+
cost.b += count;
|
|
131
|
+
break;
|
|
132
|
+
case 3 /* Blue */:
|
|
133
|
+
cost.u += count;
|
|
134
|
+
break;
|
|
135
|
+
case 1 /* Green */:
|
|
136
|
+
cost.g += count;
|
|
137
|
+
break;
|
|
138
|
+
case 0 /* Red */:
|
|
139
|
+
cost.r += count;
|
|
140
|
+
break;
|
|
141
|
+
case 4 /* White */:
|
|
142
|
+
cost.w += count;
|
|
143
|
+
break;
|
|
144
|
+
case 5 /* Colorless */:
|
|
145
|
+
cost.c += count;
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function manaCostKey(cost) {
|
|
150
|
+
return `${cost.r},${cost.g},${cost.b},${cost.u},${cost.w},${cost.c},${cost.bits}`;
|
|
151
|
+
}
|
|
152
|
+
function compareManaCost(a, b) {
|
|
153
|
+
if (a.bits !== b.bits) return a.bits - b.bits;
|
|
154
|
+
if (a.r !== b.r) return a.r - b.r;
|
|
155
|
+
if (a.w !== b.w) return a.w - b.w;
|
|
156
|
+
if (a.b !== b.b) return a.b - b.b;
|
|
157
|
+
if (a.u !== b.u) return a.u - b.u;
|
|
158
|
+
if (a.g !== b.g) return a.g - b.g;
|
|
159
|
+
return a.c - b.c;
|
|
160
|
+
}
|
|
161
|
+
function manaCostsFromStr(manaCostStr) {
|
|
162
|
+
const symbolStack = manaCostSymbolsFromStr(manaCostStr);
|
|
163
|
+
const results = /* @__PURE__ */ new Map();
|
|
164
|
+
manaCostsFromStrRecur(results, emptyManaCost(), symbolStack, 0);
|
|
165
|
+
return [...results.values()].sort(compareManaCost);
|
|
166
|
+
}
|
|
167
|
+
function manaCostsFromStrRecur(results, current, symbolStack, idx) {
|
|
168
|
+
if (symbolStack.length <= idx) {
|
|
169
|
+
const updated = updateManaCostBits(current);
|
|
170
|
+
results.set(manaCostKey(updated), updated);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const [leftSym, rightSym] = symbolStack[idx];
|
|
174
|
+
const left = {
|
|
175
|
+
bits: 0,
|
|
176
|
+
r: leftSym.r + current.r,
|
|
177
|
+
g: leftSym.g + current.g,
|
|
178
|
+
b: leftSym.b + current.b,
|
|
179
|
+
u: leftSym.u + current.u,
|
|
180
|
+
w: leftSym.w + current.w,
|
|
181
|
+
c: leftSym.c + current.c
|
|
182
|
+
};
|
|
183
|
+
manaCostsFromStrRecur(results, left, symbolStack, idx + 1);
|
|
184
|
+
if (rightSym) {
|
|
185
|
+
const right = {
|
|
186
|
+
bits: 0,
|
|
187
|
+
r: rightSym.r + current.r,
|
|
188
|
+
g: rightSym.g + current.g,
|
|
189
|
+
b: rightSym.b + current.b,
|
|
190
|
+
u: rightSym.u + current.u,
|
|
191
|
+
w: rightSym.w + current.w,
|
|
192
|
+
c: rightSym.c + current.c
|
|
193
|
+
};
|
|
194
|
+
manaCostsFromStrRecur(results, right, symbolStack, idx + 1);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function manaCostSymbolsFromStr(manaCostStr) {
|
|
198
|
+
let sigil = "";
|
|
199
|
+
const symbolStack = [];
|
|
200
|
+
let shouldPushRight = false;
|
|
201
|
+
let idx = 0;
|
|
202
|
+
for (const ch of manaCostStr) {
|
|
203
|
+
switch (ch) {
|
|
204
|
+
case "{": {
|
|
205
|
+
sigil = "";
|
|
206
|
+
symbolStack.push([emptyManaCost(), null]);
|
|
207
|
+
idx = symbolStack.length - 1;
|
|
208
|
+
shouldPushRight = false;
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
case "/":
|
|
212
|
+
case "\\": {
|
|
213
|
+
const color = manaColorFromStr(sigil);
|
|
214
|
+
const count = Number.parseInt(sigil, 10);
|
|
215
|
+
const n = Number.isFinite(count) ? count : 1;
|
|
216
|
+
const cost = emptyManaCost();
|
|
217
|
+
applyColor(cost, color, n);
|
|
218
|
+
symbolStack[idx][0] = cost;
|
|
219
|
+
shouldPushRight = true;
|
|
220
|
+
sigil = "";
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
case "}": {
|
|
224
|
+
const color = manaColorFromStr(sigil);
|
|
225
|
+
const count = Number.parseInt(sigil, 10);
|
|
226
|
+
const n = Number.isFinite(count) ? count : 1;
|
|
227
|
+
const cost = emptyManaCost();
|
|
228
|
+
applyColor(cost, color, n);
|
|
229
|
+
if (shouldPushRight) {
|
|
230
|
+
symbolStack[idx][1] = cost;
|
|
231
|
+
} else {
|
|
232
|
+
symbolStack[idx][0] = cost;
|
|
233
|
+
}
|
|
234
|
+
sigil = "";
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
default:
|
|
238
|
+
sigil += ch;
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return symbolStack;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// src/card/types.ts
|
|
246
|
+
var CardKind = /* @__PURE__ */ ((CardKind2) => {
|
|
247
|
+
CardKind2["BasicLand"] = "BasicLand";
|
|
248
|
+
CardKind2["TapLand"] = "TapLand";
|
|
249
|
+
CardKind2["CheckLand"] = "CheckLand";
|
|
250
|
+
CardKind2["ShockLand"] = "ShockLand";
|
|
251
|
+
CardKind2["FastLand"] = "FastLand";
|
|
252
|
+
CardKind2["SlowLand"] = "SlowLand";
|
|
253
|
+
CardKind2["BattleLand"] = "BattleLand";
|
|
254
|
+
CardKind2["TurnLand"] = "TurnLand";
|
|
255
|
+
CardKind2["SurveilLand"] = "SurveilLand";
|
|
256
|
+
CardKind2["BounceLand"] = "BounceLand";
|
|
257
|
+
CardKind2["TriomeLand"] = "TriomeLand";
|
|
258
|
+
CardKind2["CyclingLand"] = "CyclingLand";
|
|
259
|
+
CardKind2["PainLand"] = "PainLand";
|
|
260
|
+
CardKind2["FetchLand"] = "FetchLand";
|
|
261
|
+
CardKind2["CanopyLand"] = "CanopyLand";
|
|
262
|
+
CardKind2["PathwayLand"] = "PathwayLand";
|
|
263
|
+
CardKind2["OtherLand"] = "OtherLand";
|
|
264
|
+
CardKind2["ForcedLand"] = "ForcedLand";
|
|
265
|
+
CardKind2["Creature"] = "Creature";
|
|
266
|
+
CardKind2["Spell"] = "Spell";
|
|
267
|
+
CardKind2["Enchantment"] = "Enchantment";
|
|
268
|
+
CardKind2["Instant"] = "Instant";
|
|
269
|
+
CardKind2["Planeswalker"] = "Planeswalker";
|
|
270
|
+
CardKind2["Sorcery"] = "Sorcery";
|
|
271
|
+
CardKind2["Artifact"] = "Artifact";
|
|
272
|
+
CardKind2["Unknown"] = "Unknown";
|
|
273
|
+
return CardKind2;
|
|
274
|
+
})(CardKind || {});
|
|
275
|
+
var Rarity = /* @__PURE__ */ ((Rarity2) => {
|
|
276
|
+
Rarity2["Common"] = "common";
|
|
277
|
+
Rarity2["Uncommon"] = "uncommon";
|
|
278
|
+
Rarity2["Rare"] = "rare";
|
|
279
|
+
Rarity2["Mythic"] = "mythic";
|
|
280
|
+
Rarity2["Special"] = "special";
|
|
281
|
+
Rarity2["Bonus"] = "bonus";
|
|
282
|
+
Rarity2["Other"] = "other";
|
|
283
|
+
return Rarity2;
|
|
284
|
+
})(Rarity || {});
|
|
285
|
+
function cloneCard(card) {
|
|
286
|
+
return {
|
|
287
|
+
...card,
|
|
288
|
+
manaCost: { ...card.manaCost },
|
|
289
|
+
allManaCosts: card.allManaCosts.map((c) => ({ ...c })),
|
|
290
|
+
basicLandTypes: card.basicLandTypes ?? 0,
|
|
291
|
+
checkTypes: card.checkTypes ?? 0,
|
|
292
|
+
manaPerTap: card.manaPerTap ?? 1
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
function isLandKind(kind) {
|
|
296
|
+
return kind === "BasicLand" /* BasicLand */ || kind === "ShockLand" /* ShockLand */ || kind === "CheckLand" /* CheckLand */ || kind === "TapLand" /* TapLand */ || kind === "FastLand" /* FastLand */ || kind === "SlowLand" /* SlowLand */ || kind === "BattleLand" /* BattleLand */ || kind === "TurnLand" /* TurnLand */ || kind === "SurveilLand" /* SurveilLand */ || kind === "BounceLand" /* BounceLand */ || kind === "TriomeLand" /* TriomeLand */ || kind === "CyclingLand" /* CyclingLand */ || kind === "PainLand" /* PainLand */ || kind === "FetchLand" /* FetchLand */ || kind === "CanopyLand" /* CanopyLand */ || kind === "PathwayLand" /* PathwayLand */ || kind === "OtherLand" /* OtherLand */ || kind === "ForcedLand" /* ForcedLand */;
|
|
297
|
+
}
|
|
298
|
+
function isLand(card) {
|
|
299
|
+
return isLandKind(card.kind);
|
|
300
|
+
}
|
|
301
|
+
function hashCardName(name) {
|
|
302
|
+
let hash = 0xcbf29ce484222325n;
|
|
303
|
+
const prime = 0x100000001b3n;
|
|
304
|
+
for (let i = 0; i < name.length; i++) {
|
|
305
|
+
hash ^= BigInt(name.charCodeAt(i));
|
|
306
|
+
hash = BigInt.asUintN(64, hash * prime);
|
|
307
|
+
}
|
|
308
|
+
return Number(hash & 0x1fffffffffffffn);
|
|
309
|
+
}
|
|
310
|
+
function parseRarity(raw) {
|
|
311
|
+
switch (raw.toLowerCase()) {
|
|
312
|
+
case "common":
|
|
313
|
+
return "common" /* Common */;
|
|
314
|
+
case "uncommon":
|
|
315
|
+
return "uncommon" /* Uncommon */;
|
|
316
|
+
case "rare":
|
|
317
|
+
return "rare" /* Rare */;
|
|
318
|
+
case "mythic":
|
|
319
|
+
return "mythic" /* Mythic */;
|
|
320
|
+
case "special":
|
|
321
|
+
return "special" /* Special */;
|
|
322
|
+
case "bonus":
|
|
323
|
+
return "bonus" /* Bonus */;
|
|
324
|
+
default:
|
|
325
|
+
return "other" /* Other */;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
function parseSetCode(raw) {
|
|
329
|
+
return raw.toLowerCase();
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// src/card/land-types.ts
|
|
333
|
+
var BasicLandType = {
|
|
334
|
+
Plains: 1 << 0,
|
|
335
|
+
Island: 1 << 1,
|
|
336
|
+
Swamp: 1 << 2,
|
|
337
|
+
Mountain: 1 << 3,
|
|
338
|
+
Forest: 1 << 4
|
|
339
|
+
};
|
|
340
|
+
var TYPE_NAME_TO_BIT = [
|
|
341
|
+
["plains", BasicLandType.Plains],
|
|
342
|
+
["island", BasicLandType.Island],
|
|
343
|
+
["swamp", BasicLandType.Swamp],
|
|
344
|
+
["mountain", BasicLandType.Mountain],
|
|
345
|
+
["forest", BasicLandType.Forest]
|
|
346
|
+
];
|
|
347
|
+
function basicLandTypesFromTypeLine(typeLine) {
|
|
348
|
+
const lower = typeLine.toLowerCase();
|
|
349
|
+
let bits2 = 0;
|
|
350
|
+
for (const [name, bit] of TYPE_NAME_TO_BIT) {
|
|
351
|
+
if (lower.includes(name)) bits2 |= bit;
|
|
352
|
+
}
|
|
353
|
+
return bits2;
|
|
354
|
+
}
|
|
355
|
+
function basicLandTypeCount(typeLine) {
|
|
356
|
+
const bits2 = basicLandTypesFromTypeLine(typeLine);
|
|
357
|
+
let n = 0;
|
|
358
|
+
for (let b = bits2; b !== 0; b >>= 1) n += b & 1;
|
|
359
|
+
return n;
|
|
360
|
+
}
|
|
361
|
+
function checkTypesFromOracleText(oracleText) {
|
|
362
|
+
const lower = oracleText.toLowerCase();
|
|
363
|
+
const idx = lower.indexOf("unless you control");
|
|
364
|
+
if (idx === -1) return 0;
|
|
365
|
+
const clause = lower.slice(idx);
|
|
366
|
+
let bits2 = 0;
|
|
367
|
+
for (const [name, bit] of TYPE_NAME_TO_BIT) {
|
|
368
|
+
if (clause.includes(name)) bits2 |= bit;
|
|
369
|
+
}
|
|
370
|
+
return bits2;
|
|
371
|
+
}
|
|
372
|
+
function normalizeOracleForEtb(oracleText) {
|
|
373
|
+
return oracleText.toLowerCase().replace(/enters the battlefield tapped/g, "enters tapped").replace(/\s+/g, " ").trim();
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// src/card/mana-color-count.ts
|
|
377
|
+
function newManaColorCount() {
|
|
378
|
+
return {
|
|
379
|
+
total: 0,
|
|
380
|
+
b: 0,
|
|
381
|
+
u: 0,
|
|
382
|
+
g: 0,
|
|
383
|
+
r: 0,
|
|
384
|
+
w: 0,
|
|
385
|
+
c: 0,
|
|
386
|
+
wu: 0,
|
|
387
|
+
wb: 0,
|
|
388
|
+
ub: 0,
|
|
389
|
+
ur: 0,
|
|
390
|
+
br: 0,
|
|
391
|
+
bg: 0,
|
|
392
|
+
rg: 0,
|
|
393
|
+
rw: 0,
|
|
394
|
+
gw: 0,
|
|
395
|
+
gu: 0
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
function countManaColor(mcc, card) {
|
|
399
|
+
mcc.total += 1;
|
|
400
|
+
mcc.u += card.u;
|
|
401
|
+
mcc.r += card.r;
|
|
402
|
+
mcc.b += card.b;
|
|
403
|
+
mcc.g += card.g;
|
|
404
|
+
mcc.w += card.w;
|
|
405
|
+
mcc.c += card.c;
|
|
406
|
+
const key = `${card.r},${card.g},${card.b},${card.u},${card.w}`;
|
|
407
|
+
switch (key) {
|
|
408
|
+
case "1,1,0,0,0":
|
|
409
|
+
mcc.rg += 1;
|
|
410
|
+
break;
|
|
411
|
+
case "1,0,1,0,0":
|
|
412
|
+
mcc.br += 1;
|
|
413
|
+
break;
|
|
414
|
+
case "1,0,0,1,0":
|
|
415
|
+
mcc.ur += 1;
|
|
416
|
+
break;
|
|
417
|
+
case "1,0,0,0,1":
|
|
418
|
+
mcc.rw += 1;
|
|
419
|
+
break;
|
|
420
|
+
case "0,1,1,0,0":
|
|
421
|
+
mcc.bg += 1;
|
|
422
|
+
break;
|
|
423
|
+
case "0,1,0,1,0":
|
|
424
|
+
mcc.gu += 1;
|
|
425
|
+
break;
|
|
426
|
+
case "0,1,0,0,1":
|
|
427
|
+
mcc.gw += 1;
|
|
428
|
+
break;
|
|
429
|
+
case "0,0,1,1,0":
|
|
430
|
+
mcc.ub += 1;
|
|
431
|
+
break;
|
|
432
|
+
case "0,0,1,0,1":
|
|
433
|
+
mcc.wb += 1;
|
|
434
|
+
break;
|
|
435
|
+
case "0,0,0,1,1":
|
|
436
|
+
mcc.wu += 1;
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// node_modules/fflate/esm/index.mjs
|
|
442
|
+
var import_module = require("module");
|
|
443
|
+
var require2 = (0, import_module.createRequire)("/");
|
|
444
|
+
var _a;
|
|
445
|
+
var Worker;
|
|
446
|
+
var isMarkedAsUntransferable;
|
|
447
|
+
try {
|
|
448
|
+
_a = require2("worker_threads"), Worker = _a.Worker, isMarkedAsUntransferable = _a.isMarkedAsUntransferable;
|
|
449
|
+
} catch (e) {
|
|
450
|
+
}
|
|
451
|
+
var u8 = Uint8Array;
|
|
452
|
+
var u16 = Uint16Array;
|
|
453
|
+
var i32 = Int32Array;
|
|
454
|
+
var fleb = new u8([
|
|
455
|
+
0,
|
|
456
|
+
0,
|
|
457
|
+
0,
|
|
458
|
+
0,
|
|
459
|
+
0,
|
|
460
|
+
0,
|
|
461
|
+
0,
|
|
462
|
+
0,
|
|
463
|
+
1,
|
|
464
|
+
1,
|
|
465
|
+
1,
|
|
466
|
+
1,
|
|
467
|
+
2,
|
|
468
|
+
2,
|
|
469
|
+
2,
|
|
470
|
+
2,
|
|
471
|
+
3,
|
|
472
|
+
3,
|
|
473
|
+
3,
|
|
474
|
+
3,
|
|
475
|
+
4,
|
|
476
|
+
4,
|
|
477
|
+
4,
|
|
478
|
+
4,
|
|
479
|
+
5,
|
|
480
|
+
5,
|
|
481
|
+
5,
|
|
482
|
+
5,
|
|
483
|
+
0,
|
|
484
|
+
/* unused */
|
|
485
|
+
0,
|
|
486
|
+
0,
|
|
487
|
+
/* impossible */
|
|
488
|
+
0
|
|
489
|
+
]);
|
|
490
|
+
var fdeb = new u8([
|
|
491
|
+
0,
|
|
492
|
+
0,
|
|
493
|
+
0,
|
|
494
|
+
0,
|
|
495
|
+
1,
|
|
496
|
+
1,
|
|
497
|
+
2,
|
|
498
|
+
2,
|
|
499
|
+
3,
|
|
500
|
+
3,
|
|
501
|
+
4,
|
|
502
|
+
4,
|
|
503
|
+
5,
|
|
504
|
+
5,
|
|
505
|
+
6,
|
|
506
|
+
6,
|
|
507
|
+
7,
|
|
508
|
+
7,
|
|
509
|
+
8,
|
|
510
|
+
8,
|
|
511
|
+
9,
|
|
512
|
+
9,
|
|
513
|
+
10,
|
|
514
|
+
10,
|
|
515
|
+
11,
|
|
516
|
+
11,
|
|
517
|
+
12,
|
|
518
|
+
12,
|
|
519
|
+
13,
|
|
520
|
+
13,
|
|
521
|
+
/* unused */
|
|
522
|
+
0,
|
|
523
|
+
0
|
|
524
|
+
]);
|
|
525
|
+
var clim = new u8([16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]);
|
|
526
|
+
var freb = function(eb, start) {
|
|
527
|
+
var b = new u16(31);
|
|
528
|
+
for (var i = 0; i < 31; ++i) {
|
|
529
|
+
b[i] = start += 1 << eb[i - 1];
|
|
530
|
+
}
|
|
531
|
+
var r = new i32(b[30]);
|
|
532
|
+
for (var i = 1; i < 30; ++i) {
|
|
533
|
+
for (var j = b[i]; j < b[i + 1]; ++j) {
|
|
534
|
+
r[j] = j - b[i] << 5 | i;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return { b, r };
|
|
538
|
+
};
|
|
539
|
+
var _a = freb(fleb, 2);
|
|
540
|
+
var fl = _a.b;
|
|
541
|
+
var revfl = _a.r;
|
|
542
|
+
fl[28] = 258, revfl[258] = 28;
|
|
543
|
+
var _b = freb(fdeb, 0);
|
|
544
|
+
var fd = _b.b;
|
|
545
|
+
var revfd = _b.r;
|
|
546
|
+
var rev = new u16(32768);
|
|
547
|
+
for (i = 0; i < 32768; ++i) {
|
|
548
|
+
x = (i & 43690) >> 1 | (i & 21845) << 1;
|
|
549
|
+
x = (x & 52428) >> 2 | (x & 13107) << 2;
|
|
550
|
+
x = (x & 61680) >> 4 | (x & 3855) << 4;
|
|
551
|
+
rev[i] = ((x & 65280) >> 8 | (x & 255) << 8) >> 1;
|
|
552
|
+
}
|
|
553
|
+
var x;
|
|
554
|
+
var i;
|
|
555
|
+
var hMap = (function(cd, mb, r) {
|
|
556
|
+
var s = cd.length;
|
|
557
|
+
var i = 0;
|
|
558
|
+
var l = new u16(mb);
|
|
559
|
+
for (; i < s; ++i) {
|
|
560
|
+
if (cd[i])
|
|
561
|
+
++l[cd[i] - 1];
|
|
562
|
+
}
|
|
563
|
+
var le = new u16(mb);
|
|
564
|
+
for (i = 1; i < mb; ++i) {
|
|
565
|
+
le[i] = le[i - 1] + l[i - 1] << 1;
|
|
566
|
+
}
|
|
567
|
+
var co;
|
|
568
|
+
if (r) {
|
|
569
|
+
co = new u16(1 << mb);
|
|
570
|
+
var rvb = 15 - mb;
|
|
571
|
+
for (i = 0; i < s; ++i) {
|
|
572
|
+
if (cd[i]) {
|
|
573
|
+
var sv = i << 4 | cd[i];
|
|
574
|
+
var r_1 = mb - cd[i];
|
|
575
|
+
var v = le[cd[i] - 1]++ << r_1;
|
|
576
|
+
for (var m = v | (1 << r_1) - 1; v <= m; ++v) {
|
|
577
|
+
co[rev[v] >> rvb] = sv;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
} else {
|
|
582
|
+
co = new u16(s);
|
|
583
|
+
for (i = 0; i < s; ++i) {
|
|
584
|
+
if (cd[i]) {
|
|
585
|
+
co[i] = rev[le[cd[i] - 1]++] >> 15 - cd[i];
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return co;
|
|
590
|
+
});
|
|
591
|
+
var flt = new u8(288);
|
|
592
|
+
for (i = 0; i < 144; ++i)
|
|
593
|
+
flt[i] = 8;
|
|
594
|
+
var i;
|
|
595
|
+
for (i = 144; i < 256; ++i)
|
|
596
|
+
flt[i] = 9;
|
|
597
|
+
var i;
|
|
598
|
+
for (i = 256; i < 280; ++i)
|
|
599
|
+
flt[i] = 7;
|
|
600
|
+
var i;
|
|
601
|
+
for (i = 280; i < 288; ++i)
|
|
602
|
+
flt[i] = 8;
|
|
603
|
+
var i;
|
|
604
|
+
var fdt = new u8(32);
|
|
605
|
+
for (i = 0; i < 32; ++i)
|
|
606
|
+
fdt[i] = 5;
|
|
607
|
+
var i;
|
|
608
|
+
var flrm = /* @__PURE__ */ hMap(flt, 9, 1);
|
|
609
|
+
var fdrm = /* @__PURE__ */ hMap(fdt, 5, 1);
|
|
610
|
+
var max = function(a) {
|
|
611
|
+
var m = a[0];
|
|
612
|
+
for (var i = 1; i < a.length; ++i) {
|
|
613
|
+
if (a[i] > m)
|
|
614
|
+
m = a[i];
|
|
615
|
+
}
|
|
616
|
+
return m;
|
|
617
|
+
};
|
|
618
|
+
var bits = function(d, p, m) {
|
|
619
|
+
var o = p / 8 | 0;
|
|
620
|
+
return (d[o] | d[o + 1] << 8) >> (p & 7) & m;
|
|
621
|
+
};
|
|
622
|
+
var bits16 = function(d, p) {
|
|
623
|
+
var o = p / 8 | 0;
|
|
624
|
+
return (d[o] | d[o + 1] << 8 | d[o + 2] << 16) >> (p & 7);
|
|
625
|
+
};
|
|
626
|
+
var shft = function(p) {
|
|
627
|
+
return (p + 7) / 8 | 0;
|
|
628
|
+
};
|
|
629
|
+
var slc = function(v, s, e) {
|
|
630
|
+
if (s == null || s < 0)
|
|
631
|
+
s = 0;
|
|
632
|
+
if (e == null || e > v.length)
|
|
633
|
+
e = v.length;
|
|
634
|
+
return new u8(v.subarray(s, e));
|
|
635
|
+
};
|
|
636
|
+
var ec = [
|
|
637
|
+
"unexpected EOF",
|
|
638
|
+
"invalid block type",
|
|
639
|
+
"invalid length/literal",
|
|
640
|
+
"invalid distance",
|
|
641
|
+
"stream finished",
|
|
642
|
+
"no stream handler",
|
|
643
|
+
,
|
|
644
|
+
// determined by compression function
|
|
645
|
+
"no callback",
|
|
646
|
+
"invalid UTF-8 data",
|
|
647
|
+
"extra field too long",
|
|
648
|
+
"date not in range 1980-2099",
|
|
649
|
+
"filename too long",
|
|
650
|
+
"stream finishing",
|
|
651
|
+
"invalid zip data"
|
|
652
|
+
// determined by unknown compression method
|
|
653
|
+
];
|
|
654
|
+
var err = function(ind, msg, nt) {
|
|
655
|
+
var e = new Error(msg || ec[ind]);
|
|
656
|
+
e.code = ind;
|
|
657
|
+
if (Error.captureStackTrace)
|
|
658
|
+
Error.captureStackTrace(e, err);
|
|
659
|
+
if (!nt)
|
|
660
|
+
throw e;
|
|
661
|
+
return e;
|
|
662
|
+
};
|
|
663
|
+
var inflt = function(dat, st, buf, dict) {
|
|
664
|
+
var sl = dat.length, dl = dict ? dict.length : 0;
|
|
665
|
+
if (!sl || st.f && !st.l)
|
|
666
|
+
return buf || new u8(0);
|
|
667
|
+
var noBuf = !buf;
|
|
668
|
+
var resize = noBuf || st.i != 2;
|
|
669
|
+
var noSt = st.i;
|
|
670
|
+
if (noBuf)
|
|
671
|
+
buf = new u8(sl * 3);
|
|
672
|
+
var cbuf = function(l2) {
|
|
673
|
+
var bl = buf.length;
|
|
674
|
+
if (l2 > bl) {
|
|
675
|
+
var nbuf = new u8(Math.max(bl * 2, l2));
|
|
676
|
+
nbuf.set(buf);
|
|
677
|
+
buf = nbuf;
|
|
678
|
+
}
|
|
679
|
+
};
|
|
680
|
+
var final = st.f || 0, pos = st.p || 0, bt = st.b || 0, lm = st.l, dm = st.d, lbt = st.m, dbt = st.n;
|
|
681
|
+
var tbts = sl * 8;
|
|
682
|
+
do {
|
|
683
|
+
if (!lm) {
|
|
684
|
+
final = bits(dat, pos, 1);
|
|
685
|
+
var type = bits(dat, pos + 1, 3);
|
|
686
|
+
pos += 3;
|
|
687
|
+
if (!type) {
|
|
688
|
+
var s = shft(pos) + 4, l = dat[s - 4] | dat[s - 3] << 8, t = s + l;
|
|
689
|
+
if (t > sl) {
|
|
690
|
+
if (noSt)
|
|
691
|
+
err(0);
|
|
692
|
+
break;
|
|
693
|
+
}
|
|
694
|
+
if (resize)
|
|
695
|
+
cbuf(bt + l);
|
|
696
|
+
buf.set(dat.subarray(s, t), bt);
|
|
697
|
+
st.b = bt += l, st.p = pos = t * 8, st.f = final;
|
|
698
|
+
continue;
|
|
699
|
+
} else if (type == 1)
|
|
700
|
+
lm = flrm, dm = fdrm, lbt = 9, dbt = 5;
|
|
701
|
+
else if (type == 2) {
|
|
702
|
+
var hLit = bits(dat, pos, 31) + 257, hcLen = bits(dat, pos + 10, 15) + 4;
|
|
703
|
+
var tl = hLit + bits(dat, pos + 5, 31) + 1;
|
|
704
|
+
pos += 14;
|
|
705
|
+
var ldt = new u8(tl);
|
|
706
|
+
var clt = new u8(19);
|
|
707
|
+
for (var i = 0; i < hcLen; ++i) {
|
|
708
|
+
clt[clim[i]] = bits(dat, pos + i * 3, 7);
|
|
709
|
+
}
|
|
710
|
+
pos += hcLen * 3;
|
|
711
|
+
var clb = max(clt), clbmsk = (1 << clb) - 1;
|
|
712
|
+
var clm = hMap(clt, clb, 1);
|
|
713
|
+
for (var i = 0; i < tl; ) {
|
|
714
|
+
var r = clm[bits(dat, pos, clbmsk)];
|
|
715
|
+
pos += r & 15;
|
|
716
|
+
var s = r >> 4;
|
|
717
|
+
if (s < 16) {
|
|
718
|
+
ldt[i++] = s;
|
|
719
|
+
} else {
|
|
720
|
+
var c = 0, n = 0;
|
|
721
|
+
if (s == 16)
|
|
722
|
+
n = 3 + bits(dat, pos, 3), pos += 2, c = ldt[i - 1];
|
|
723
|
+
else if (s == 17)
|
|
724
|
+
n = 3 + bits(dat, pos, 7), pos += 3;
|
|
725
|
+
else if (s == 18)
|
|
726
|
+
n = 11 + bits(dat, pos, 127), pos += 7;
|
|
727
|
+
while (n--)
|
|
728
|
+
ldt[i++] = c;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
var lt = ldt.subarray(0, hLit), dt = ldt.subarray(hLit);
|
|
732
|
+
lbt = max(lt);
|
|
733
|
+
dbt = max(dt);
|
|
734
|
+
lm = hMap(lt, lbt, 1);
|
|
735
|
+
dm = hMap(dt, dbt, 1);
|
|
736
|
+
} else
|
|
737
|
+
err(1);
|
|
738
|
+
if (pos > tbts) {
|
|
739
|
+
if (noSt)
|
|
740
|
+
err(0);
|
|
741
|
+
break;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
if (resize)
|
|
745
|
+
cbuf(bt + 131072);
|
|
746
|
+
var lms = (1 << lbt) - 1, dms = (1 << dbt) - 1;
|
|
747
|
+
var lpos = pos;
|
|
748
|
+
for (; ; lpos = pos) {
|
|
749
|
+
var c = lm[bits16(dat, pos) & lms], sym = c >> 4;
|
|
750
|
+
pos += c & 15;
|
|
751
|
+
if (pos > tbts) {
|
|
752
|
+
if (noSt)
|
|
753
|
+
err(0);
|
|
754
|
+
break;
|
|
755
|
+
}
|
|
756
|
+
if (!c)
|
|
757
|
+
err(2);
|
|
758
|
+
if (sym < 256)
|
|
759
|
+
buf[bt++] = sym;
|
|
760
|
+
else if (sym == 256) {
|
|
761
|
+
lpos = pos, lm = null;
|
|
762
|
+
break;
|
|
763
|
+
} else {
|
|
764
|
+
var add = sym - 254;
|
|
765
|
+
if (sym > 264) {
|
|
766
|
+
var i = sym - 257, b = fleb[i];
|
|
767
|
+
add = bits(dat, pos, (1 << b) - 1) + fl[i];
|
|
768
|
+
pos += b;
|
|
769
|
+
}
|
|
770
|
+
var d = dm[bits16(dat, pos) & dms], dsym = d >> 4;
|
|
771
|
+
if (!d)
|
|
772
|
+
err(3);
|
|
773
|
+
pos += d & 15;
|
|
774
|
+
var dt = fd[dsym];
|
|
775
|
+
if (dsym > 3) {
|
|
776
|
+
var b = fdeb[dsym];
|
|
777
|
+
dt += bits16(dat, pos) & (1 << b) - 1, pos += b;
|
|
778
|
+
}
|
|
779
|
+
if (pos > tbts) {
|
|
780
|
+
if (noSt)
|
|
781
|
+
err(0);
|
|
782
|
+
break;
|
|
783
|
+
}
|
|
784
|
+
if (resize)
|
|
785
|
+
cbuf(bt + 131072);
|
|
786
|
+
var end = bt + add;
|
|
787
|
+
if (bt < dt) {
|
|
788
|
+
var shift = dl - dt, dend = Math.min(dt, end);
|
|
789
|
+
if (shift + bt < 0)
|
|
790
|
+
err(3);
|
|
791
|
+
for (; bt < dend; ++bt)
|
|
792
|
+
buf[bt] = dict[shift + bt];
|
|
793
|
+
}
|
|
794
|
+
for (; bt < end; ++bt)
|
|
795
|
+
buf[bt] = buf[bt - dt];
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
st.l = lm, st.p = lpos, st.b = bt, st.f = final;
|
|
799
|
+
if (lm)
|
|
800
|
+
final = 1, st.m = lbt, st.d = dm, st.n = dbt;
|
|
801
|
+
} while (!final);
|
|
802
|
+
return bt != buf.length && noBuf ? slc(buf, 0, bt) : buf.subarray(0, bt);
|
|
803
|
+
};
|
|
804
|
+
var et = /* @__PURE__ */ new u8(0);
|
|
805
|
+
var gzs = function(d) {
|
|
806
|
+
if (d[0] != 31 || d[1] != 139 || d[2] != 8)
|
|
807
|
+
err(6, "invalid gzip data");
|
|
808
|
+
var flg = d[3];
|
|
809
|
+
var st = 10;
|
|
810
|
+
if (flg & 4)
|
|
811
|
+
st += (d[10] | d[11] << 8) + 2;
|
|
812
|
+
for (var zs = (flg >> 3 & 1) + (flg >> 4 & 1); zs > 0; zs -= !d[st++])
|
|
813
|
+
;
|
|
814
|
+
return st + (flg & 2);
|
|
815
|
+
};
|
|
816
|
+
var gzl = function(d) {
|
|
817
|
+
var l = d.length;
|
|
818
|
+
return (d[l - 4] | d[l - 3] << 8 | d[l - 2] << 16 | d[l - 1] << 24) >>> 0;
|
|
819
|
+
};
|
|
820
|
+
function gunzipSync(data, opts) {
|
|
821
|
+
var st = gzs(data);
|
|
822
|
+
if (st + 8 > data.length)
|
|
823
|
+
err(6, "invalid gzip data");
|
|
824
|
+
return inflt(data.subarray(st, -8), { i: 2 }, opts && opts.out || new u8(gzl(data)), opts && opts.dictionary);
|
|
825
|
+
}
|
|
826
|
+
var td = typeof TextDecoder != "undefined" && /* @__PURE__ */ new TextDecoder();
|
|
827
|
+
var tds = 0;
|
|
828
|
+
try {
|
|
829
|
+
td.decode(et, { stream: true });
|
|
830
|
+
tds = 1;
|
|
831
|
+
} catch (e) {
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// src/data.ts
|
|
835
|
+
var import_node_fs = require("fs");
|
|
836
|
+
var import_node_path = require("path");
|
|
837
|
+
var import_node_url = require("url");
|
|
838
|
+
|
|
839
|
+
// src/collection.ts
|
|
840
|
+
function collectionFromCards(cards) {
|
|
841
|
+
const sorted = [...cards].sort(
|
|
842
|
+
(a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())
|
|
843
|
+
);
|
|
844
|
+
return { cards: sorted };
|
|
845
|
+
}
|
|
846
|
+
function cardFromName(collection, name) {
|
|
847
|
+
const nameLower = name.toLowerCase();
|
|
848
|
+
let lo = 0;
|
|
849
|
+
let hi = collection.cards.length;
|
|
850
|
+
while (lo < hi) {
|
|
851
|
+
const mid = lo + hi >> 1;
|
|
852
|
+
const probe = collection.cards[mid].name.toLowerCase();
|
|
853
|
+
if (probe.localeCompare(nameLower) < 0) lo = mid + 1;
|
|
854
|
+
else hi = mid;
|
|
855
|
+
}
|
|
856
|
+
const found = collection.cards[lo];
|
|
857
|
+
if (found && found.name.toLowerCase() === nameLower) return found;
|
|
858
|
+
return void 0;
|
|
859
|
+
}
|
|
860
|
+
function otherFacesByName(collection, name) {
|
|
861
|
+
const card = cardFromName(collection, name);
|
|
862
|
+
if (!card || card.oracleId === "") return [];
|
|
863
|
+
return collection.cards.filter((c) => c.oracleId === card.oracleId && c.name !== card.name);
|
|
864
|
+
}
|
|
865
|
+
function asCollectionApi(collection) {
|
|
866
|
+
return {
|
|
867
|
+
...collection,
|
|
868
|
+
cardFromName: (name) => cardFromName(collection, name),
|
|
869
|
+
otherFacesByName: (name) => otherFacesByName(collection, name),
|
|
870
|
+
get length() {
|
|
871
|
+
return collection.cards.length;
|
|
872
|
+
}
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// src/data.ts
|
|
877
|
+
function resolveDataPath() {
|
|
878
|
+
const here = (0, import_node_path.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
879
|
+
const candidates = [
|
|
880
|
+
(0, import_node_path.join)(here, "../data/all_cards.json.gz"),
|
|
881
|
+
(0, import_node_path.join)(process.cwd(), "data/all_cards.json.gz")
|
|
882
|
+
];
|
|
883
|
+
for (const p of candidates) {
|
|
884
|
+
if ((0, import_node_fs.existsSync)(p)) return p;
|
|
885
|
+
}
|
|
886
|
+
throw new Error("Cannot find data/all_cards.json.gz \u2014 run `npm run card-update` first");
|
|
887
|
+
}
|
|
888
|
+
function loadCardsFromGzipBytes(bytes) {
|
|
889
|
+
const jsonBytes = gunzipSync(bytes);
|
|
890
|
+
const text = new TextDecoder().decode(jsonBytes);
|
|
891
|
+
const cards = JSON.parse(text);
|
|
892
|
+
return asCollectionApi(collectionFromCards(cards));
|
|
893
|
+
}
|
|
894
|
+
function loadCardsFromFile(path) {
|
|
895
|
+
const buf = (0, import_node_fs.readFileSync)(path);
|
|
896
|
+
return loadCardsFromGzipBytes(new Uint8Array(buf));
|
|
897
|
+
}
|
|
898
|
+
var cached = null;
|
|
899
|
+
function getAllCards() {
|
|
900
|
+
if (!cached) {
|
|
901
|
+
cached = loadCardsFromFile(resolveDataPath());
|
|
902
|
+
}
|
|
903
|
+
return cached;
|
|
904
|
+
}
|
|
905
|
+
function setAllCards(collection) {
|
|
906
|
+
cached = collection;
|
|
907
|
+
}
|
|
908
|
+
var ALL_CARDS = new Proxy({}, {
|
|
909
|
+
get(_target, prop, receiver) {
|
|
910
|
+
const real = getAllCards();
|
|
911
|
+
const value = Reflect.get(real, prop, receiver);
|
|
912
|
+
return typeof value === "function" ? value.bind(real) : value;
|
|
913
|
+
}
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
// src/deck.ts
|
|
917
|
+
var DeckcodeError = class extends Error {
|
|
918
|
+
/**
|
|
919
|
+
* All unresolved card names from this parse (set/collector stripped).
|
|
920
|
+
* Prefer this for UI lists when multiple cards are missing.
|
|
921
|
+
*/
|
|
922
|
+
unknownCardNames;
|
|
923
|
+
/** First unknown card name; same as `unknownCardNames[0]` when present. */
|
|
924
|
+
unknownCardName;
|
|
925
|
+
constructor(message, options) {
|
|
926
|
+
super(message);
|
|
927
|
+
this.name = "DeckcodeError";
|
|
928
|
+
const names = options?.unknownCardNames ?? (options?.unknownCardName ? [options.unknownCardName] : []);
|
|
929
|
+
this.unknownCardNames = names;
|
|
930
|
+
this.unknownCardName = names[0] ?? options?.unknownCardName;
|
|
931
|
+
}
|
|
932
|
+
};
|
|
933
|
+
function newDeck() {
|
|
934
|
+
return {
|
|
935
|
+
title: null,
|
|
936
|
+
url: null,
|
|
937
|
+
cards: [],
|
|
938
|
+
format: "standard" /* Standard */,
|
|
939
|
+
cardCount: 0
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
function deckBuilderBuild(builder) {
|
|
943
|
+
const deck = newDeck();
|
|
944
|
+
let count = 0;
|
|
945
|
+
for (const { card, count: c } of builder.values()) {
|
|
946
|
+
deck.cards.push({ card, count: c });
|
|
947
|
+
count += c;
|
|
948
|
+
}
|
|
949
|
+
deck.cardCount = count;
|
|
950
|
+
deck.cards.sort((a, b) => a.card.name.localeCompare(b.card.name));
|
|
951
|
+
return deck;
|
|
952
|
+
}
|
|
953
|
+
function insertCount(builder, card, count) {
|
|
954
|
+
const existing = builder.get(card.name);
|
|
955
|
+
if (existing) existing.count += count;
|
|
956
|
+
else builder.set(card.name, { card, count });
|
|
957
|
+
}
|
|
958
|
+
function deckFromCards(cards) {
|
|
959
|
+
const builder = /* @__PURE__ */ new Map();
|
|
960
|
+
for (const card of cards) insertCount(builder, card, 1);
|
|
961
|
+
return deckBuilderBuild(builder);
|
|
962
|
+
}
|
|
963
|
+
function deckFlatten(deck) {
|
|
964
|
+
const result = [];
|
|
965
|
+
for (const cc of deck.cards) {
|
|
966
|
+
for (let i = 0; i < cc.count; i++) result.push(cc.card);
|
|
967
|
+
}
|
|
968
|
+
return result;
|
|
969
|
+
}
|
|
970
|
+
function deckIsEmpty(deck) {
|
|
971
|
+
return deck.cardCount === 0;
|
|
972
|
+
}
|
|
973
|
+
function deckIter(deck) {
|
|
974
|
+
return deck.cards;
|
|
975
|
+
}
|
|
976
|
+
function stripInlineModifiersFromName(name) {
|
|
977
|
+
for (const sep of [" M=", " m=", " T=", " t=", " X=", " x="]) {
|
|
978
|
+
const idx = name.lastIndexOf(sep);
|
|
979
|
+
if (idx !== -1) return name.slice(0, idx).trim();
|
|
980
|
+
}
|
|
981
|
+
return name.trim();
|
|
982
|
+
}
|
|
983
|
+
function applyLandFaceFromCard(card, landFace) {
|
|
984
|
+
card.manaCost = { ...landFace.manaCost };
|
|
985
|
+
card.allManaCosts = landFace.allManaCosts.map((c) => ({ ...c }));
|
|
986
|
+
card.manaCostString = landFace.manaCostString;
|
|
987
|
+
card.turn = landFace.turn;
|
|
988
|
+
card.kind = "ForcedLand" /* ForcedLand */;
|
|
989
|
+
card.basicLandTypes = landFace.basicLandTypes ?? 0;
|
|
990
|
+
card.checkTypes = landFace.checkTypes ?? 0;
|
|
991
|
+
}
|
|
992
|
+
function applyLandFace(card, leftCardName, line) {
|
|
993
|
+
const landFace = ALL_CARDS.otherFacesByName(leftCardName).find((c) => isLand(c));
|
|
994
|
+
if (!landFace) {
|
|
995
|
+
throw new DeckcodeError(
|
|
996
|
+
`M=auto specified but no land face found for "${leftCardName}" at line ${line}`
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
applyLandFaceFromCard(card, landFace);
|
|
1000
|
+
}
|
|
1001
|
+
var INLINE_M = /\sM\s*=\s*(auto|(?:\{[WUBRGC\d]+\})+)/i;
|
|
1002
|
+
var INLINE_T = /\sT\s*=\s*(\d+)/i;
|
|
1003
|
+
var INLINE_X = /\sX\s*=\s*(\d+)/i;
|
|
1004
|
+
function inlineModifier(line, letter) {
|
|
1005
|
+
const re = letter === "M" ? INLINE_M : letter === "T" ? INLINE_T : INLINE_X;
|
|
1006
|
+
return line.match(re)?.[1];
|
|
1007
|
+
}
|
|
1008
|
+
var ARENA_LINE_REGEX = /^\s*(?<amount>\d+)\s+(?<name>[^#(\n\r]+)(?:\s*\((?<set>\w+)\)\s+(?<setnum>\d+))?\s*#?(?:\s*[Xx]\s*=\s*(?<X>\d+))?(?:\s*[Tt]\s*=\s*(?<T>\d+))?(?:\s*[Mm]\s*=\s*(?<M>auto|(?:\{[WUBRGC\d]+\})+))?/;
|
|
1009
|
+
function deckFromList(list) {
|
|
1010
|
+
const builder = /* @__PURE__ */ new Map();
|
|
1011
|
+
let lookingForDeckLine = false;
|
|
1012
|
+
const unknownCardNames = [];
|
|
1013
|
+
for (const line of list.trim().split(/\r?\n/)) {
|
|
1014
|
+
const trimmed = line.trim();
|
|
1015
|
+
const trimmedLower = trimmed.toLowerCase();
|
|
1016
|
+
if (trimmedLower === "deck") {
|
|
1017
|
+
lookingForDeckLine = false;
|
|
1018
|
+
continue;
|
|
1019
|
+
}
|
|
1020
|
+
if (trimmedLower === "commander" || trimmedLower === "companion" || trimmedLower === "about") {
|
|
1021
|
+
lookingForDeckLine = true;
|
|
1022
|
+
continue;
|
|
1023
|
+
}
|
|
1024
|
+
if (trimmedLower === "sideboard" || trimmedLower === "maybeboard") break;
|
|
1025
|
+
if (trimmed.startsWith("#")) continue;
|
|
1026
|
+
if (lookingForDeckLine) continue;
|
|
1027
|
+
if (trimmed.length === 0) break;
|
|
1028
|
+
const caps = ARENA_LINE_REGEX.exec(trimmed);
|
|
1029
|
+
if (!caps?.groups) {
|
|
1030
|
+
throw new DeckcodeError(`Cannot regex capture deck list line: ${line}`);
|
|
1031
|
+
}
|
|
1032
|
+
const amount = Number.parseInt(caps.groups.amount, 10);
|
|
1033
|
+
if (!Number.isFinite(amount)) {
|
|
1034
|
+
throw new DeckcodeError(`Cannot parse card amount from deck list line: ${line}`);
|
|
1035
|
+
}
|
|
1036
|
+
const name = stripInlineModifiersFromName(caps.groups.name.trim());
|
|
1037
|
+
const set = caps.groups.set ? parseSetCode(caps.groups.set) : "";
|
|
1038
|
+
const leftCardName = name.split("//")[0]?.trim();
|
|
1039
|
+
if (!leftCardName) {
|
|
1040
|
+
throw new DeckcodeError(`Cannot parse card name from deck list line: ${line}`);
|
|
1041
|
+
}
|
|
1042
|
+
const found = ALL_CARDS.cardFromName(leftCardName);
|
|
1043
|
+
if (!found) {
|
|
1044
|
+
if (!unknownCardNames.includes(name)) unknownCardNames.push(name);
|
|
1045
|
+
continue;
|
|
1046
|
+
}
|
|
1047
|
+
const card = cloneCard(found);
|
|
1048
|
+
const xValStr = caps.groups.X ?? inlineModifier(trimmed, "X");
|
|
1049
|
+
if (xValStr !== void 0 && card.manaCostString.includes("X")) {
|
|
1050
|
+
const xVal = Number.parseInt(xValStr, 10);
|
|
1051
|
+
if (!Number.isFinite(xVal)) {
|
|
1052
|
+
throw new DeckcodeError(`Cannot parse X= value from deck list line: ${line}`);
|
|
1053
|
+
}
|
|
1054
|
+
card.manaCost = { ...card.manaCost, c: xVal };
|
|
1055
|
+
card.allManaCosts = card.allManaCosts.map((cost) => ({ ...cost, c: xVal }));
|
|
1056
|
+
card.manaCostString = card.manaCostString.replace(/X/g, String(xVal));
|
|
1057
|
+
card.turn = manaCostCmc(card.manaCost);
|
|
1058
|
+
}
|
|
1059
|
+
const mValStr = caps.groups.M ?? inlineModifier(trimmed, "M");
|
|
1060
|
+
if (mValStr !== void 0) {
|
|
1061
|
+
if (mValStr.toLowerCase() === "auto") {
|
|
1062
|
+
applyLandFace(card, leftCardName, line);
|
|
1063
|
+
} else {
|
|
1064
|
+
const allManaCosts = manaCostsFromStr(mValStr);
|
|
1065
|
+
if (allManaCosts.length === 0) {
|
|
1066
|
+
throw new DeckcodeError(`Problematic mana cost ('M = ') specified at line ${line}`);
|
|
1067
|
+
}
|
|
1068
|
+
card.manaCost = { ...allManaCosts[0] };
|
|
1069
|
+
card.allManaCosts = allManaCosts.map((c) => ({ ...c }));
|
|
1070
|
+
card.turn = manaCostCmc(card.manaCost);
|
|
1071
|
+
card.kind = "ForcedLand" /* ForcedLand */;
|
|
1072
|
+
card.basicLandTypes = 0;
|
|
1073
|
+
card.checkTypes = 0;
|
|
1074
|
+
}
|
|
1075
|
+
} else if (!isLand(card)) {
|
|
1076
|
+
const landFace = ALL_CARDS.otherFacesByName(leftCardName).find((c) => isLand(c));
|
|
1077
|
+
if (landFace) applyLandFaceFromCard(card, landFace);
|
|
1078
|
+
}
|
|
1079
|
+
const turnValStr = caps.groups.T ?? inlineModifier(trimmed, "T");
|
|
1080
|
+
if (turnValStr !== void 0) {
|
|
1081
|
+
const turnVal = Number.parseInt(turnValStr, 10);
|
|
1082
|
+
if (!Number.isFinite(turnVal)) {
|
|
1083
|
+
throw new DeckcodeError(`Cannot parse T= value from deck list line: ${line}`);
|
|
1084
|
+
}
|
|
1085
|
+
card.turn += turnVal;
|
|
1086
|
+
}
|
|
1087
|
+
card.set = set;
|
|
1088
|
+
insertCount(builder, card, amount);
|
|
1089
|
+
}
|
|
1090
|
+
if (unknownCardNames.length > 0) {
|
|
1091
|
+
const message = unknownCardNames.length === 1 ? `Cannot find card named "${unknownCardNames[0]}" in collection` : `Cannot find cards in collection: ${unknownCardNames.map((n) => `"${n}"`).join(", ")}`;
|
|
1092
|
+
throw new DeckcodeError(message, { unknownCardNames });
|
|
1093
|
+
}
|
|
1094
|
+
return deckBuilderBuild(builder);
|
|
1095
|
+
}
|
|
1096
|
+
function decklist(list) {
|
|
1097
|
+
return deckFromList(list);
|
|
1098
|
+
}
|
|
1099
|
+
function parseDecklist(list) {
|
|
1100
|
+
try {
|
|
1101
|
+
const deck = deckFromList(list);
|
|
1102
|
+
if (deckIsEmpty(deck)) {
|
|
1103
|
+
return { ok: false, error: new DeckcodeError("Empty deckcode") };
|
|
1104
|
+
}
|
|
1105
|
+
return { ok: true, deck };
|
|
1106
|
+
} catch (e) {
|
|
1107
|
+
if (e instanceof DeckcodeError) return { ok: false, error: e };
|
|
1108
|
+
return { ok: false, error: new DeckcodeError(String(e)) };
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// src/mulligan/types.ts
|
|
1113
|
+
function createMulberry32(seed) {
|
|
1114
|
+
let t = seed >>> 0;
|
|
1115
|
+
return {
|
|
1116
|
+
genRange(min, max2) {
|
|
1117
|
+
t = t + 1831565813 >>> 0;
|
|
1118
|
+
let r = Math.imul(t ^ t >>> 15, 1 | t);
|
|
1119
|
+
r ^= r + Math.imul(r ^ r >>> 7, 61 | r);
|
|
1120
|
+
const u = ((r ^ r >>> 14) >>> 0) / 4294967296;
|
|
1121
|
+
return min + Math.floor(u * (max2 - min));
|
|
1122
|
+
}
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
function createEntropyRng() {
|
|
1126
|
+
return createMulberry32(Math.random() * 4294967295 >>> 0);
|
|
1127
|
+
}
|
|
1128
|
+
function partialShuffle(slice, rng, amount) {
|
|
1129
|
+
const n = Math.min(amount, slice.length);
|
|
1130
|
+
for (let i = 0; i < n; i++) {
|
|
1131
|
+
const j = rng.genRange(i, slice.length);
|
|
1132
|
+
const tmp = slice[i];
|
|
1133
|
+
slice[i] = slice[j];
|
|
1134
|
+
slice[j] = tmp;
|
|
1135
|
+
}
|
|
1136
|
+
return slice.slice(0, n);
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// src/bipartite.ts
|
|
1140
|
+
function maximumBipartiteMatching(edges, mCount, nCount, seen, matches) {
|
|
1141
|
+
let matchCount = 0;
|
|
1142
|
+
for (let i = 0; i < matches.length; i++) matches[i] = -1;
|
|
1143
|
+
for (let m = 0; m < mCount; m++) {
|
|
1144
|
+
for (let i = 0; i < seen.length; i++) seen[i] = false;
|
|
1145
|
+
if (recursiveFindMatch(edges, nCount, m, seen, matches)) {
|
|
1146
|
+
matchCount += 1;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
return matchCount;
|
|
1150
|
+
}
|
|
1151
|
+
function recursiveFindMatch(edges, nCount, m, seen, matches) {
|
|
1152
|
+
for (let n = 0; n < nCount; n++) {
|
|
1153
|
+
const i = nCount * m + n;
|
|
1154
|
+
if (edges[i] !== 0 && !seen[n]) {
|
|
1155
|
+
seen[n] = true;
|
|
1156
|
+
const prior = matches[n];
|
|
1157
|
+
const available = prior < 0 || recursiveFindMatch(edges, nCount, prior, seen, matches);
|
|
1158
|
+
if (available) {
|
|
1159
|
+
matches[n] = m;
|
|
1160
|
+
return true;
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
return false;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
// src/hand.ts
|
|
1168
|
+
function simCardFromCard(card) {
|
|
1169
|
+
return {
|
|
1170
|
+
hash: card.hash,
|
|
1171
|
+
kind: card.kind,
|
|
1172
|
+
manaCost: { ...card.manaCost },
|
|
1173
|
+
basicLandTypes: card.basicLandTypes ?? 0,
|
|
1174
|
+
checkTypes: card.checkTypes ?? 0,
|
|
1175
|
+
manaPerTap: card.manaPerTap ?? 1
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
var PlayOrder = /* @__PURE__ */ ((PlayOrder2) => {
|
|
1179
|
+
PlayOrder2["First"] = "First";
|
|
1180
|
+
PlayOrder2["Second"] = "Second";
|
|
1181
|
+
return PlayOrder2;
|
|
1182
|
+
})(PlayOrder || {});
|
|
1183
|
+
function newScratch(maxLandCount, maxPipCount) {
|
|
1184
|
+
return {
|
|
1185
|
+
lands: [],
|
|
1186
|
+
edges: new Array(maxLandCount * maxPipCount).fill(0),
|
|
1187
|
+
seen: new Array(maxLandCount).fill(false),
|
|
1188
|
+
matches: new Array(maxLandCount).fill(-1)
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1191
|
+
function handFromOpeningAndDraws(opening, draws, startingHandSize = 7) {
|
|
1192
|
+
const cards = [];
|
|
1193
|
+
for (const card of opening) cards.push(simCardFromCard(card));
|
|
1194
|
+
for (const card of draws) cards.push(simCardFromCard(card));
|
|
1195
|
+
const openingHandSize = opening.length;
|
|
1196
|
+
return {
|
|
1197
|
+
cards,
|
|
1198
|
+
startingHandSize,
|
|
1199
|
+
openingHandSize,
|
|
1200
|
+
mulliganCount: startingHandSize - openingHandSize
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
function handFromMulligan(mulligan, rng, deck, draws) {
|
|
1204
|
+
return mulligan.simulateHand(rng, deck, draws);
|
|
1205
|
+
}
|
|
1206
|
+
function handOpening(hand) {
|
|
1207
|
+
return hand.cards.slice(0, hand.openingHandSize);
|
|
1208
|
+
}
|
|
1209
|
+
function handDraws(hand, drawCount) {
|
|
1210
|
+
return hand.cards.slice(hand.openingHandSize, hand.openingHandSize + drawCount);
|
|
1211
|
+
}
|
|
1212
|
+
function handOpeningWithDraws(hand, drawCount) {
|
|
1213
|
+
return hand.cards.slice(0, hand.openingHandSize + drawCount);
|
|
1214
|
+
}
|
|
1215
|
+
function countInOpeningWithDraws(hand, drawCount, predicate) {
|
|
1216
|
+
let count = 0;
|
|
1217
|
+
for (const card of handOpeningWithDraws(hand, drawCount)) {
|
|
1218
|
+
if (predicate(card)) count += 1;
|
|
1219
|
+
}
|
|
1220
|
+
return count;
|
|
1221
|
+
}
|
|
1222
|
+
function autoTapByTurn(hand, goal, turn, playOrder) {
|
|
1223
|
+
const scratch = newScratch(30, 8);
|
|
1224
|
+
return autoTapWithScratch(hand, simCardFromCard(goal), turn, playOrder, scratch);
|
|
1225
|
+
}
|
|
1226
|
+
function playCmcAutoTap(hand, goal) {
|
|
1227
|
+
return autoTapByTurn(hand, goal, Math.max(1, goal.turn), "First" /* First */);
|
|
1228
|
+
}
|
|
1229
|
+
function drawCmcAutoTap(hand, goal) {
|
|
1230
|
+
return autoTapByTurn(hand, goal, Math.max(1, goal.turn), "Second" /* Second */);
|
|
1231
|
+
}
|
|
1232
|
+
function isAlwaysAvailableKind(kind) {
|
|
1233
|
+
return kind === "BasicLand" /* BasicLand */ || kind === "OtherLand" /* OtherLand */ || kind === "ForcedLand" /* ForcedLand */ || kind === "ShockLand" /* ShockLand */ || kind === "PainLand" /* PainLand */ || kind === "FetchLand" /* FetchLand */ || kind === "CanopyLand" /* CanopyLand */ || kind === "PathwayLand" /* PathwayLand */;
|
|
1234
|
+
}
|
|
1235
|
+
function isConditionalEtbKind(kind) {
|
|
1236
|
+
return kind === "TapLand" /* TapLand */ || kind === "CheckLand" /* CheckLand */ || kind === "FastLand" /* FastLand */ || kind === "SlowLand" /* SlowLand */ || kind === "BattleLand" /* BattleLand */ || kind === "TurnLand" /* TurnLand */ || kind === "SurveilLand" /* SurveilLand */ || kind === "BounceLand" /* BounceLand */ || kind === "TriomeLand" /* TriomeLand */ || kind === "CyclingLand" /* CyclingLand */;
|
|
1237
|
+
}
|
|
1238
|
+
function pushLandColumns(lands, card) {
|
|
1239
|
+
const copies = card.manaPerTap ?? 1;
|
|
1240
|
+
for (let k = 0; k < copies; k++) lands.push(card);
|
|
1241
|
+
}
|
|
1242
|
+
function availableTurnForLand(card, playTurn, otherLands, boardTypes, basicsOnBoard = 0) {
|
|
1243
|
+
switch (card.kind) {
|
|
1244
|
+
case "TapLand" /* TapLand */:
|
|
1245
|
+
case "SurveilLand" /* SurveilLand */:
|
|
1246
|
+
case "BounceLand" /* BounceLand */:
|
|
1247
|
+
case "TriomeLand" /* TriomeLand */:
|
|
1248
|
+
case "CyclingLand" /* CyclingLand */:
|
|
1249
|
+
return playTurn + 1;
|
|
1250
|
+
case "CheckLand" /* CheckLand */: {
|
|
1251
|
+
const unlocked = (boardTypes & card.checkTypes) !== 0;
|
|
1252
|
+
return unlocked ? playTurn : playTurn + 1;
|
|
1253
|
+
}
|
|
1254
|
+
case "FastLand" /* FastLand */:
|
|
1255
|
+
return otherLands <= 2 ? playTurn : playTurn + 1;
|
|
1256
|
+
case "SlowLand" /* SlowLand */:
|
|
1257
|
+
return otherLands >= 2 ? playTurn : playTurn + 1;
|
|
1258
|
+
case "BattleLand" /* BattleLand */:
|
|
1259
|
+
return basicsOnBoard >= 2 ? playTurn : playTurn + 1;
|
|
1260
|
+
case "TurnLand" /* TurnLand */:
|
|
1261
|
+
return playTurn <= 3 ? playTurn : playTurn + 1;
|
|
1262
|
+
default:
|
|
1263
|
+
return playTurn;
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
function scheduleLandPlays(openingHand, drawn, playOrder) {
|
|
1267
|
+
const candidates = [];
|
|
1268
|
+
let openingLandIdx = 0;
|
|
1269
|
+
for (const card of openingHand) {
|
|
1270
|
+
if (!isLandKind(card.kind)) continue;
|
|
1271
|
+
openingLandIdx += 1;
|
|
1272
|
+
candidates.push({ card, earliest: openingLandIdx });
|
|
1273
|
+
}
|
|
1274
|
+
for (let drawIdx = 0; drawIdx < drawn.length; drawIdx++) {
|
|
1275
|
+
const card = drawn[drawIdx];
|
|
1276
|
+
if (!isLandKind(card.kind)) continue;
|
|
1277
|
+
const earliest = playOrder === "First" /* First */ ? drawIdx + 2 : drawIdx + 1;
|
|
1278
|
+
candidates.push({ card, earliest });
|
|
1279
|
+
}
|
|
1280
|
+
const scheduled = [];
|
|
1281
|
+
let lastPlayTurn = 0;
|
|
1282
|
+
for (const { card, earliest } of candidates) {
|
|
1283
|
+
const playTurn = Math.max(earliest, lastPlayTurn + 1);
|
|
1284
|
+
scheduled.push({ card, playTurn });
|
|
1285
|
+
lastPlayTurn = playTurn;
|
|
1286
|
+
}
|
|
1287
|
+
return scheduled;
|
|
1288
|
+
}
|
|
1289
|
+
function autoTapWithScratch(hand, goal, turn, playOrder, scratch) {
|
|
1290
|
+
const drawCount = playOrder === "First" /* First */ ? turn - 1 : turn;
|
|
1291
|
+
const openingHand = handOpening(hand);
|
|
1292
|
+
const drawn = handDraws(hand, drawCount);
|
|
1293
|
+
let inOpeningHand = false;
|
|
1294
|
+
let inDrawHand = false;
|
|
1295
|
+
for (const card of openingHand) {
|
|
1296
|
+
if (card.hash === goal.hash) inOpeningHand = true;
|
|
1297
|
+
}
|
|
1298
|
+
for (const card of drawn) {
|
|
1299
|
+
if (card.hash === goal.hash) inDrawHand = true;
|
|
1300
|
+
}
|
|
1301
|
+
const scheduled = scheduleLandPlays(openingHand, drawn, playOrder);
|
|
1302
|
+
scratch.lands.length = 0;
|
|
1303
|
+
let boardTypes = 0;
|
|
1304
|
+
let basicsOnBoard = 0;
|
|
1305
|
+
for (let i = 0; i < scheduled.length; i++) {
|
|
1306
|
+
const { card, playTurn } = scheduled[i];
|
|
1307
|
+
const otherLands = i;
|
|
1308
|
+
if (isAlwaysAvailableKind(card.kind)) {
|
|
1309
|
+
pushLandColumns(scratch.lands, card);
|
|
1310
|
+
} else if (isConditionalEtbKind(card.kind)) {
|
|
1311
|
+
const availableTurn = availableTurnForLand(
|
|
1312
|
+
card,
|
|
1313
|
+
playTurn,
|
|
1314
|
+
otherLands,
|
|
1315
|
+
boardTypes,
|
|
1316
|
+
basicsOnBoard
|
|
1317
|
+
);
|
|
1318
|
+
if (availableTurn <= turn) pushLandColumns(scratch.lands, card);
|
|
1319
|
+
} else {
|
|
1320
|
+
pushLandColumns(scratch.lands, card);
|
|
1321
|
+
}
|
|
1322
|
+
boardTypes |= card.basicLandTypes;
|
|
1323
|
+
if (card.kind === "BasicLand" /* BasicLand */) basicsOnBoard += 1;
|
|
1324
|
+
}
|
|
1325
|
+
const pipCount = goal.manaCost.r + goal.manaCost.g + goal.manaCost.b + goal.manaCost.u + goal.manaCost.w + goal.manaCost.c;
|
|
1326
|
+
const landCount = scratch.lands.length;
|
|
1327
|
+
if (landCount < pipCount) {
|
|
1328
|
+
return { paid: false, cmc: false, inOpeningHand, inDrawHand };
|
|
1329
|
+
}
|
|
1330
|
+
const edgeLen = pipCount * landCount;
|
|
1331
|
+
if (scratch.edges.length !== edgeLen) {
|
|
1332
|
+
scratch.edges = new Array(edgeLen).fill(0);
|
|
1333
|
+
} else {
|
|
1334
|
+
scratch.edges.fill(0);
|
|
1335
|
+
}
|
|
1336
|
+
if (scratch.seen.length !== landCount) {
|
|
1337
|
+
scratch.seen = new Array(landCount).fill(false);
|
|
1338
|
+
}
|
|
1339
|
+
if (scratch.matches.length !== landCount) {
|
|
1340
|
+
scratch.matches = new Array(landCount).fill(-1);
|
|
1341
|
+
}
|
|
1342
|
+
let row = 0;
|
|
1343
|
+
for (let m = 0; m < goal.manaCost.r; m++, row++) {
|
|
1344
|
+
for (let n = 0; n < landCount; n++) {
|
|
1345
|
+
scratch.edges[landCount * row + n] = scratch.lands[n].manaCost.r;
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
for (let m = 0; m < goal.manaCost.g; m++, row++) {
|
|
1349
|
+
for (let n = 0; n < landCount; n++) {
|
|
1350
|
+
scratch.edges[landCount * row + n] = scratch.lands[n].manaCost.g;
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
for (let m = 0; m < goal.manaCost.b; m++, row++) {
|
|
1354
|
+
for (let n = 0; n < landCount; n++) {
|
|
1355
|
+
scratch.edges[landCount * row + n] = scratch.lands[n].manaCost.b;
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
for (let m = 0; m < goal.manaCost.u; m++, row++) {
|
|
1359
|
+
for (let n = 0; n < landCount; n++) {
|
|
1360
|
+
scratch.edges[landCount * row + n] = scratch.lands[n].manaCost.u;
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
for (let m = 0; m < goal.manaCost.w; m++, row++) {
|
|
1364
|
+
for (let n = 0; n < landCount; n++) {
|
|
1365
|
+
scratch.edges[landCount * row + n] = scratch.lands[n].manaCost.w;
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
for (let m = 0; m < goal.manaCost.c; m++, row++) {
|
|
1369
|
+
for (let n = 0; n < landCount; n++) {
|
|
1370
|
+
scratch.edges[landCount * row + n] = 1;
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
const pipsPaid = maximumBipartiteMatching(
|
|
1374
|
+
scratch.edges,
|
|
1375
|
+
pipCount,
|
|
1376
|
+
landCount,
|
|
1377
|
+
scratch.seen,
|
|
1378
|
+
scratch.matches
|
|
1379
|
+
);
|
|
1380
|
+
return {
|
|
1381
|
+
paid: pipsPaid === pipCount,
|
|
1382
|
+
cmc: true,
|
|
1383
|
+
inOpeningHand,
|
|
1384
|
+
inDrawHand
|
|
1385
|
+
};
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
// src/mulligan/london.ts
|
|
1389
|
+
var STARTING_HAND_SIZE = 7;
|
|
1390
|
+
function londonNever() {
|
|
1391
|
+
return {
|
|
1392
|
+
startingHandSize: STARTING_HAND_SIZE,
|
|
1393
|
+
mulliganDownTo: STARTING_HAND_SIZE,
|
|
1394
|
+
mulliganOnLands: /* @__PURE__ */ new Set(),
|
|
1395
|
+
acceptableHandList: []
|
|
1396
|
+
};
|
|
1397
|
+
}
|
|
1398
|
+
function londonSimulateHand(self, rng, deck, draws) {
|
|
1399
|
+
const deckSize = deck.length;
|
|
1400
|
+
const startingHandSize = Math.min(self.startingHandSize, deckSize);
|
|
1401
|
+
const mulliganDownTo = Math.min(self.mulliganDownTo, startingHandSize);
|
|
1402
|
+
const maxMulliganRounds = startingHandSize - mulliganDownTo + 1;
|
|
1403
|
+
const cardsToDraw = Math.min(startingHandSize + draws + maxMulliganRounds, deckSize);
|
|
1404
|
+
const indexRange = Array.from({ length: deckSize }, (_, i) => i);
|
|
1405
|
+
const mustKeepCardIndices = [];
|
|
1406
|
+
const seenCardHashes = /* @__PURE__ */ new Set();
|
|
1407
|
+
for (let round = 0; round < maxMulliganRounds; round++) {
|
|
1408
|
+
const shuffledIndices2 = partialShuffle(indexRange.slice(), rng, cardsToDraw);
|
|
1409
|
+
const shuffledDeck2 = shuffledIndices2.map((i) => deck[i]);
|
|
1410
|
+
const startingHand = shuffledDeck2.slice(0, startingHandSize);
|
|
1411
|
+
const isLastRound = round === maxMulliganRounds - 1;
|
|
1412
|
+
let landCount = 0;
|
|
1413
|
+
for (const c of startingHand) {
|
|
1414
|
+
if (isLand(c)) landCount += 1;
|
|
1415
|
+
}
|
|
1416
|
+
const sufficientLandCount = !self.mulliganOnLands.has(landCount);
|
|
1417
|
+
if (!isLastRound && !sufficientLandCount) continue;
|
|
1418
|
+
let foundAcceptableHand = false;
|
|
1419
|
+
for (const acceptableHand of self.acceptableHandList) {
|
|
1420
|
+
mustKeepCardIndices.length = 0;
|
|
1421
|
+
seenCardHashes.clear();
|
|
1422
|
+
for (let i = 0; i < startingHand.length; i++) {
|
|
1423
|
+
const card = startingHand[i];
|
|
1424
|
+
if (seenCardHashes.has(card.hash)) continue;
|
|
1425
|
+
if (acceptableHand.has(card.hash)) mustKeepCardIndices.push(i);
|
|
1426
|
+
seenCardHashes.add(card.hash);
|
|
1427
|
+
}
|
|
1428
|
+
foundAcceptableHand = mustKeepCardIndices.length === acceptableHand.size;
|
|
1429
|
+
if (foundAcceptableHand) break;
|
|
1430
|
+
}
|
|
1431
|
+
const disregardFoundAcceptableHand = self.acceptableHandList.length === 0;
|
|
1432
|
+
const keep = isLastRound || sufficientLandCount && (disregardFoundAcceptableHand || foundAcceptableHand);
|
|
1433
|
+
if (!keep) continue;
|
|
1434
|
+
const openingHandSize = startingHandSize - round;
|
|
1435
|
+
let landsSaved = 0;
|
|
1436
|
+
for (let i = 0; i < startingHand.length; i++) {
|
|
1437
|
+
const card = startingHand[i];
|
|
1438
|
+
if (!isLandKind(card.kind)) continue;
|
|
1439
|
+
const needMoreLands = self.mulliganOnLands.has(landsSaved) && landsSaved < openingHandSize;
|
|
1440
|
+
if (needMoreLands) {
|
|
1441
|
+
mustKeepCardIndices.push(i);
|
|
1442
|
+
landsSaved += 1;
|
|
1443
|
+
} else {
|
|
1444
|
+
break;
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
mustKeepCardIndices.sort((a, b) => a - b);
|
|
1448
|
+
{
|
|
1449
|
+
let w = 0;
|
|
1450
|
+
for (let r = 0; r < mustKeepCardIndices.length; r++) {
|
|
1451
|
+
if (r === 0 || mustKeepCardIndices[r] !== mustKeepCardIndices[r - 1]) {
|
|
1452
|
+
mustKeepCardIndices[w++] = mustKeepCardIndices[r];
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
mustKeepCardIndices.length = w;
|
|
1456
|
+
}
|
|
1457
|
+
for (let i = 0; i < mustKeepCardIndices.length; i++) {
|
|
1458
|
+
const mustKeepI = mustKeepCardIndices[i];
|
|
1459
|
+
const tmp = shuffledDeck2[i];
|
|
1460
|
+
shuffledDeck2[i] = shuffledDeck2[mustKeepI];
|
|
1461
|
+
shuffledDeck2[mustKeepI] = tmp;
|
|
1462
|
+
}
|
|
1463
|
+
let discardCount = 0;
|
|
1464
|
+
for (let i = openingHandSize; i < startingHandSize; i++) {
|
|
1465
|
+
const j = cardsToDraw - 1 - discardCount;
|
|
1466
|
+
const tmp = shuffledDeck2[i];
|
|
1467
|
+
shuffledDeck2[i] = shuffledDeck2[j];
|
|
1468
|
+
shuffledDeck2[j] = tmp;
|
|
1469
|
+
discardCount += 1;
|
|
1470
|
+
}
|
|
1471
|
+
return handFromOpeningAndDraws(
|
|
1472
|
+
shuffledDeck2.slice(0, openingHandSize),
|
|
1473
|
+
shuffledDeck2.slice(openingHandSize),
|
|
1474
|
+
startingHandSize
|
|
1475
|
+
);
|
|
1476
|
+
}
|
|
1477
|
+
const shuffledIndices = partialShuffle(indexRange.slice(), rng, cardsToDraw);
|
|
1478
|
+
const shuffledDeck = shuffledIndices.map((i) => deck[i]);
|
|
1479
|
+
return handFromOpeningAndDraws(
|
|
1480
|
+
shuffledDeck.slice(0, startingHandSize),
|
|
1481
|
+
shuffledDeck.slice(startingHandSize),
|
|
1482
|
+
startingHandSize
|
|
1483
|
+
);
|
|
1484
|
+
}
|
|
1485
|
+
function asLondonMulligan(london) {
|
|
1486
|
+
return {
|
|
1487
|
+
simulateHand: (rng, deck, draws) => londonSimulateHand(london, rng, deck, draws)
|
|
1488
|
+
};
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
// src/mulligan/never.ts
|
|
1492
|
+
var STARTING_HAND_SIZE2 = 7;
|
|
1493
|
+
function newNever() {
|
|
1494
|
+
return { startingHandSize: STARTING_HAND_SIZE2 };
|
|
1495
|
+
}
|
|
1496
|
+
function neverSimulateHand(self, rng, deck, draws) {
|
|
1497
|
+
const deckLen = deck.length;
|
|
1498
|
+
const cardsToDraw = Math.min(deckLen, self.startingHandSize + draws);
|
|
1499
|
+
const startingHandSize = Math.min(deckLen, self.startingHandSize);
|
|
1500
|
+
const indexRange = Array.from({ length: deckLen }, (_, i) => i);
|
|
1501
|
+
const shuffledIndices = partialShuffle(indexRange, rng, cardsToDraw);
|
|
1502
|
+
const shuffledDeck = shuffledIndices.map((i) => deck[i]);
|
|
1503
|
+
return handFromOpeningAndDraws(
|
|
1504
|
+
shuffledDeck.slice(0, startingHandSize),
|
|
1505
|
+
shuffledDeck.slice(startingHandSize),
|
|
1506
|
+
startingHandSize
|
|
1507
|
+
);
|
|
1508
|
+
}
|
|
1509
|
+
function asNeverMulligan(n = newNever()) {
|
|
1510
|
+
return {
|
|
1511
|
+
simulateHand: (rng, deck, draws) => neverSimulateHand(n, rng, deck, draws)
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
// src/simulation.ts
|
|
1516
|
+
var import_node_os2 = require("os");
|
|
1517
|
+
|
|
1518
|
+
// src/parallel.ts
|
|
1519
|
+
var import_node_fs2 = require("fs");
|
|
1520
|
+
var import_node_os = require("os");
|
|
1521
|
+
var import_node_path2 = require("path");
|
|
1522
|
+
var import_node_url2 = require("url");
|
|
1523
|
+
var import_node_worker_threads = require("worker_threads");
|
|
1524
|
+
function createShardWorker(payload) {
|
|
1525
|
+
const fromSrc = /[/\\]src[/\\]/.test((0, import_node_url2.fileURLToPath)(importMetaUrl));
|
|
1526
|
+
if (fromSrc) {
|
|
1527
|
+
return new import_node_worker_threads.Worker(new URL("./simulation-worker.ts", importMetaUrl), {
|
|
1528
|
+
workerData: payload,
|
|
1529
|
+
execArgv: ["--import", "tsx"]
|
|
1530
|
+
});
|
|
1531
|
+
}
|
|
1532
|
+
return new import_node_worker_threads.Worker(new URL("./simulation-worker.js", importMetaUrl), {
|
|
1533
|
+
workerData: payload
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1536
|
+
function generateHandsParallelSync(req) {
|
|
1537
|
+
const nWorkers = Math.max(1, Math.min(req.workers, req.runCount));
|
|
1538
|
+
const dir = (0, import_node_fs2.mkdtempSync)((0, import_node_path2.join)((0, import_node_os.tmpdir)(), "landlord-hands-"));
|
|
1539
|
+
const sab = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * nWorkers);
|
|
1540
|
+
const statuses = new Int32Array(sab);
|
|
1541
|
+
const workers = [];
|
|
1542
|
+
const baseSeed = req.seed ?? Math.random() * 4294967295 >>> 0;
|
|
1543
|
+
try {
|
|
1544
|
+
let remaining = req.runCount;
|
|
1545
|
+
let offset = 0;
|
|
1546
|
+
for (let i = 0; i < nWorkers; i++) {
|
|
1547
|
+
const shard = Math.floor(remaining / (nWorkers - i));
|
|
1548
|
+
remaining -= shard;
|
|
1549
|
+
const outFile = (0, import_node_path2.join)(dir, `shard-${i}.json`);
|
|
1550
|
+
const seed = baseSeed + offset * 2654435769 >>> 0;
|
|
1551
|
+
offset += shard;
|
|
1552
|
+
const payload = {
|
|
1553
|
+
outFile,
|
|
1554
|
+
statusIndex: i,
|
|
1555
|
+
sab,
|
|
1556
|
+
runCount: shard,
|
|
1557
|
+
drawCount: req.drawCount,
|
|
1558
|
+
deck: req.deck,
|
|
1559
|
+
seed,
|
|
1560
|
+
startingHandSize: req.startingHandSize,
|
|
1561
|
+
mulliganDownTo: req.mulliganDownTo,
|
|
1562
|
+
mulliganOnLands: req.mulliganOnLands,
|
|
1563
|
+
acceptableHandList: req.acceptableHandList
|
|
1564
|
+
};
|
|
1565
|
+
const worker = createShardWorker(payload);
|
|
1566
|
+
workers.push(worker);
|
|
1567
|
+
worker.on("error", () => {
|
|
1568
|
+
Atomics.store(statuses, i, -1);
|
|
1569
|
+
Atomics.notify(statuses, i);
|
|
1570
|
+
});
|
|
1571
|
+
}
|
|
1572
|
+
for (let i = 0; i < nWorkers; i++) {
|
|
1573
|
+
while (Atomics.load(statuses, i) === 0) {
|
|
1574
|
+
Atomics.wait(statuses, i, 0, 50);
|
|
1575
|
+
}
|
|
1576
|
+
if (Atomics.load(statuses, i) < 0) {
|
|
1577
|
+
throw new Error(`worker shard ${i} failed`);
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
const hands = [];
|
|
1581
|
+
for (let i = 0; i < nWorkers; i++) {
|
|
1582
|
+
const text = (0, import_node_fs2.readFileSync)((0, import_node_path2.join)(dir, `shard-${i}.json`), "utf8");
|
|
1583
|
+
const shardHands = JSON.parse(text);
|
|
1584
|
+
hands.push(...shardHands);
|
|
1585
|
+
}
|
|
1586
|
+
return hands;
|
|
1587
|
+
} finally {
|
|
1588
|
+
for (const w of workers) {
|
|
1589
|
+
void w.terminate();
|
|
1590
|
+
}
|
|
1591
|
+
try {
|
|
1592
|
+
(0, import_node_fs2.rmSync)(dir, { recursive: true, force: true });
|
|
1593
|
+
} catch {
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
// src/yield-macrotask.ts
|
|
1599
|
+
function yieldMacrotask() {
|
|
1600
|
+
return new Promise((resolve) => {
|
|
1601
|
+
setTimeout(resolve, 0);
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
// src/simulation.ts
|
|
1606
|
+
function newObservations() {
|
|
1607
|
+
return { mana: 0, cmc: 0, play: 0, inOpeningHand: 0, totalRuns: 0 };
|
|
1608
|
+
}
|
|
1609
|
+
function pMana(obs) {
|
|
1610
|
+
return obs.mana / obs.totalRuns;
|
|
1611
|
+
}
|
|
1612
|
+
function pManaGivenCmc(obs) {
|
|
1613
|
+
return obs.cmc === 0 ? 0 : obs.mana / obs.cmc;
|
|
1614
|
+
}
|
|
1615
|
+
function pPlay(obs) {
|
|
1616
|
+
return obs.play / obs.totalRuns;
|
|
1617
|
+
}
|
|
1618
|
+
function wilsonHalfWidth(successes, trials, z = 1.96) {
|
|
1619
|
+
if (trials <= 0) return 1;
|
|
1620
|
+
const p = successes / trials;
|
|
1621
|
+
const z2 = z * z;
|
|
1622
|
+
const denom = 1 + z2 / trials;
|
|
1623
|
+
const center = p + z2 / (2 * trials);
|
|
1624
|
+
const margin = z * Math.sqrt((p * (1 - p) + z2 / (4 * trials)) / trials);
|
|
1625
|
+
const hi = (center + margin) / denom;
|
|
1626
|
+
const lo = (center - margin) / denom;
|
|
1627
|
+
return (hi - lo) / 2;
|
|
1628
|
+
}
|
|
1629
|
+
var PARALLEL_THRESHOLD = 2e3;
|
|
1630
|
+
var EARLY_STOP_BATCH = 1e3;
|
|
1631
|
+
function accumulateOpeningStats(hands) {
|
|
1632
|
+
return {
|
|
1633
|
+
accumulatedOpeningHandSize: hands.reduce((sum, hand) => sum + handOpening(hand).length, 0),
|
|
1634
|
+
accumulatedOpeningHandLandCount: hands.reduce(
|
|
1635
|
+
(sum, hand) => sum + countInOpeningWithDraws(hand, 0, (c) => isLandKind(c.kind)),
|
|
1636
|
+
0
|
|
1637
|
+
)
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
function generateHandsSequential(runCount, mulligan, deck, drawCount, seed) {
|
|
1641
|
+
const rng = seed !== void 0 ? createMulberry32(seed) : createEntropyRng();
|
|
1642
|
+
const hands = [];
|
|
1643
|
+
for (let i = 0; i < runCount; i++) {
|
|
1644
|
+
hands.push(handFromMulligan(mulligan, rng, deck, drawCount));
|
|
1645
|
+
}
|
|
1646
|
+
return hands;
|
|
1647
|
+
}
|
|
1648
|
+
function shouldParallel(config) {
|
|
1649
|
+
if (config.parallel === false) return false;
|
|
1650
|
+
if (config.seed !== void 0 && config.parallel !== true) return false;
|
|
1651
|
+
if (config.parallel === true) return config.runCount >= 2;
|
|
1652
|
+
return config.runCount >= PARALLEL_THRESHOLD;
|
|
1653
|
+
}
|
|
1654
|
+
function simulationFromConfig(config) {
|
|
1655
|
+
if (config.runCount <= 0) throw new Error("runCount must be > 0");
|
|
1656
|
+
const deck = deckFlatten(config.deck);
|
|
1657
|
+
let hands;
|
|
1658
|
+
if (shouldParallel(config)) {
|
|
1659
|
+
try {
|
|
1660
|
+
hands = generateHandsParallelSync({
|
|
1661
|
+
runCount: config.runCount,
|
|
1662
|
+
drawCount: config.drawCount,
|
|
1663
|
+
deck,
|
|
1664
|
+
seed: config.seed,
|
|
1665
|
+
startingHandSize: config.startingHandSize ?? 7,
|
|
1666
|
+
mulliganDownTo: config.mulliganDownTo ?? config.startingHandSize ?? 7,
|
|
1667
|
+
mulliganOnLands: config.mulliganOnLands ?? [],
|
|
1668
|
+
acceptableHandList: config.acceptableHandList ?? [],
|
|
1669
|
+
workers: Math.min((0, import_node_os2.availableParallelism)(), config.runCount)
|
|
1670
|
+
});
|
|
1671
|
+
} catch {
|
|
1672
|
+
hands = generateHandsSequential(
|
|
1673
|
+
config.runCount,
|
|
1674
|
+
config.mulligan,
|
|
1675
|
+
deck,
|
|
1676
|
+
config.drawCount,
|
|
1677
|
+
config.seed
|
|
1678
|
+
);
|
|
1679
|
+
}
|
|
1680
|
+
} else {
|
|
1681
|
+
hands = generateHandsSequential(
|
|
1682
|
+
config.runCount,
|
|
1683
|
+
config.mulligan,
|
|
1684
|
+
deck,
|
|
1685
|
+
config.drawCount,
|
|
1686
|
+
config.seed
|
|
1687
|
+
);
|
|
1688
|
+
}
|
|
1689
|
+
const stats = accumulateOpeningStats(hands);
|
|
1690
|
+
return {
|
|
1691
|
+
hands,
|
|
1692
|
+
...stats,
|
|
1693
|
+
onThePlay: config.onThePlay
|
|
1694
|
+
};
|
|
1695
|
+
}
|
|
1696
|
+
function simulationFromConfigAdaptive(config, cards) {
|
|
1697
|
+
if (config.epsilon === void 0) return simulationFromConfig(config);
|
|
1698
|
+
const maxRuns = config.runCount;
|
|
1699
|
+
const hands = [];
|
|
1700
|
+
let seed = config.seed;
|
|
1701
|
+
const playOrder = config.onThePlay ? "First" /* First */ : "Second" /* Second */;
|
|
1702
|
+
while (hands.length < maxRuns) {
|
|
1703
|
+
const batch = Math.min(EARLY_STOP_BATCH, maxRuns - hands.length);
|
|
1704
|
+
const batchConfig = {
|
|
1705
|
+
...config,
|
|
1706
|
+
runCount: batch,
|
|
1707
|
+
seed,
|
|
1708
|
+
parallel: false
|
|
1709
|
+
};
|
|
1710
|
+
const batchSim = simulationFromConfig(batchConfig);
|
|
1711
|
+
hands.push(...batchSim.hands);
|
|
1712
|
+
if (seed !== void 0) seed = seed + batch * 2654435769 >>> 0;
|
|
1713
|
+
if (hands.length >= Math.min(EARLY_STOP_BATCH, maxRuns)) {
|
|
1714
|
+
if (aggregateWilsonMet(hands, cards, playOrder, config.epsilon)) break;
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
const stats = accumulateOpeningStats(hands);
|
|
1718
|
+
return { hands, ...stats, onThePlay: config.onThePlay };
|
|
1719
|
+
}
|
|
1720
|
+
var DEFAULT_ASYNC_BATCH = 500;
|
|
1721
|
+
function throwIfAborted(signal) {
|
|
1722
|
+
signal?.throwIfAborted();
|
|
1723
|
+
}
|
|
1724
|
+
function aggregateWilsonMet(hands, cards, playOrder, epsilon) {
|
|
1725
|
+
let mana = 0;
|
|
1726
|
+
let cmc = 0;
|
|
1727
|
+
const scratch = newScratch(30, 10);
|
|
1728
|
+
for (const card of cards) {
|
|
1729
|
+
if (isLandKind(card.kind)) continue;
|
|
1730
|
+
for (const hand of hands) {
|
|
1731
|
+
let result = {
|
|
1732
|
+
paid: false,
|
|
1733
|
+
cmc: false,
|
|
1734
|
+
inOpeningHand: false,
|
|
1735
|
+
inDrawHand: false
|
|
1736
|
+
};
|
|
1737
|
+
for (const manaCost of card.allManaCosts) {
|
|
1738
|
+
const goal = {
|
|
1739
|
+
hash: card.hash,
|
|
1740
|
+
manaCost: { ...manaCost },
|
|
1741
|
+
kind: card.kind,
|
|
1742
|
+
basicLandTypes: card.basicLandTypes ?? 0,
|
|
1743
|
+
checkTypes: card.checkTypes ?? 0,
|
|
1744
|
+
manaPerTap: card.manaPerTap ?? 1
|
|
1745
|
+
};
|
|
1746
|
+
result = autoTapWithScratch(hand, goal, card.turn, playOrder, scratch);
|
|
1747
|
+
if (result.paid) break;
|
|
1748
|
+
}
|
|
1749
|
+
if (!result.cmc) continue;
|
|
1750
|
+
cmc += 1;
|
|
1751
|
+
if (result.paid) mana += 1;
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
return cmc > 0 && wilsonHalfWidth(mana, cmc) < epsilon;
|
|
1755
|
+
}
|
|
1756
|
+
async function simulationFromConfigAsync(config, options = {}) {
|
|
1757
|
+
if (config.runCount <= 0) throw new Error("runCount must be > 0");
|
|
1758
|
+
const onProgress = options.onProgress;
|
|
1759
|
+
const signal = options.signal;
|
|
1760
|
+
const total = config.runCount;
|
|
1761
|
+
const playOrder = config.onThePlay ? "First" /* First */ : "Second" /* Second */;
|
|
1762
|
+
const shouldYield = onProgress !== void 0 || signal !== void 0;
|
|
1763
|
+
const report = async (completed) => {
|
|
1764
|
+
throwIfAborted(signal);
|
|
1765
|
+
onProgress?.({ completed, total });
|
|
1766
|
+
if (!shouldYield) return;
|
|
1767
|
+
await yieldMacrotask();
|
|
1768
|
+
throwIfAborted(signal);
|
|
1769
|
+
};
|
|
1770
|
+
if (config.epsilon !== void 0) {
|
|
1771
|
+
if (options.cards === void 0) {
|
|
1772
|
+
throw new Error("cards is required when epsilon is set");
|
|
1773
|
+
}
|
|
1774
|
+
const batchSize2 = EARLY_STOP_BATCH;
|
|
1775
|
+
const cards = options.cards;
|
|
1776
|
+
const hands2 = [];
|
|
1777
|
+
let seed = config.seed;
|
|
1778
|
+
while (hands2.length < total) {
|
|
1779
|
+
throwIfAborted(signal);
|
|
1780
|
+
const batch = Math.min(batchSize2, total - hands2.length);
|
|
1781
|
+
const batchConfig = {
|
|
1782
|
+
...config,
|
|
1783
|
+
runCount: batch,
|
|
1784
|
+
seed,
|
|
1785
|
+
parallel: false,
|
|
1786
|
+
epsilon: void 0
|
|
1787
|
+
};
|
|
1788
|
+
const batchSim = simulationFromConfig(batchConfig);
|
|
1789
|
+
hands2.push(...batchSim.hands);
|
|
1790
|
+
if (seed !== void 0) seed = seed + batch * 2654435769 >>> 0;
|
|
1791
|
+
await report(hands2.length);
|
|
1792
|
+
if (hands2.length >= Math.min(EARLY_STOP_BATCH, total) && aggregateWilsonMet(hands2, cards, playOrder, config.epsilon)) {
|
|
1793
|
+
break;
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
const stats2 = accumulateOpeningStats(hands2);
|
|
1797
|
+
return { hands: hands2, ...stats2, onThePlay: config.onThePlay };
|
|
1798
|
+
}
|
|
1799
|
+
const deck = deckFlatten(config.deck);
|
|
1800
|
+
const batchSize = Math.max(1, options.batchSize ?? DEFAULT_ASYNC_BATCH);
|
|
1801
|
+
const rng = config.seed !== void 0 ? createMulberry32(config.seed) : createEntropyRng();
|
|
1802
|
+
const hands = [];
|
|
1803
|
+
while (hands.length < total) {
|
|
1804
|
+
throwIfAborted(signal);
|
|
1805
|
+
const n = Math.min(batchSize, total - hands.length);
|
|
1806
|
+
for (let i = 0; i < n; i++) {
|
|
1807
|
+
hands.push(handFromMulligan(config.mulligan, rng, deck, config.drawCount));
|
|
1808
|
+
}
|
|
1809
|
+
await report(hands.length);
|
|
1810
|
+
}
|
|
1811
|
+
const stats = accumulateOpeningStats(hands);
|
|
1812
|
+
return { hands, ...stats, onThePlay: config.onThePlay };
|
|
1813
|
+
}
|
|
1814
|
+
function observationsForCard(sim, card) {
|
|
1815
|
+
return observationsForCardByTurn(sim, card, card.turn);
|
|
1816
|
+
}
|
|
1817
|
+
function observationsForCardByTurn(sim, card, turn) {
|
|
1818
|
+
const observations = newObservations();
|
|
1819
|
+
observations.totalRuns = sim.hands.length;
|
|
1820
|
+
const scratch = newScratch(30, 10);
|
|
1821
|
+
const playOrder = sim.onThePlay ? "First" /* First */ : "Second" /* Second */;
|
|
1822
|
+
for (const hand of sim.hands) {
|
|
1823
|
+
let result = {
|
|
1824
|
+
paid: false,
|
|
1825
|
+
cmc: false,
|
|
1826
|
+
inOpeningHand: false,
|
|
1827
|
+
inDrawHand: false
|
|
1828
|
+
};
|
|
1829
|
+
for (const manaCost of card.allManaCosts) {
|
|
1830
|
+
const goal = {
|
|
1831
|
+
hash: card.hash,
|
|
1832
|
+
manaCost: { ...manaCost },
|
|
1833
|
+
kind: card.kind,
|
|
1834
|
+
basicLandTypes: card.basicLandTypes ?? 0,
|
|
1835
|
+
checkTypes: card.checkTypes ?? 0,
|
|
1836
|
+
manaPerTap: card.manaPerTap ?? 1
|
|
1837
|
+
};
|
|
1838
|
+
result = autoTapWithScratch(hand, goal, turn, playOrder, scratch);
|
|
1839
|
+
if (result.paid) break;
|
|
1840
|
+
}
|
|
1841
|
+
if (result.inOpeningHand) observations.inOpeningHand += 1;
|
|
1842
|
+
if (!result.cmc) continue;
|
|
1843
|
+
observations.cmc += 1;
|
|
1844
|
+
if (result.paid) {
|
|
1845
|
+
observations.mana += 1;
|
|
1846
|
+
if (result.inOpeningHand || result.inDrawHand) {
|
|
1847
|
+
observations.play += 1;
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
return observations;
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
// src/observations-report.ts
|
|
1855
|
+
var COLOR_CONSTRAINED_THRESHOLD = 0.15;
|
|
1856
|
+
var DRAW_DEPENDENT_THRESHOLD = 0.15;
|
|
1857
|
+
function emptyObservationsReport() {
|
|
1858
|
+
return {
|
|
1859
|
+
totalSimulations: 0,
|
|
1860
|
+
avgOpeningHandSize: 0,
|
|
1861
|
+
avgOpeningLandCount: 0,
|
|
1862
|
+
deckSize: 0,
|
|
1863
|
+
deckAverageCmc: 0,
|
|
1864
|
+
cards: [],
|
|
1865
|
+
weakestOnCurve: [],
|
|
1866
|
+
colorConstrained: [],
|
|
1867
|
+
drawDependent: []
|
|
1868
|
+
};
|
|
1869
|
+
}
|
|
1870
|
+
function cardReportFromInput(input) {
|
|
1871
|
+
const { observations: o } = input;
|
|
1872
|
+
const total = o.totalRuns;
|
|
1873
|
+
const played = o.play;
|
|
1874
|
+
return {
|
|
1875
|
+
name: input.name,
|
|
1876
|
+
manaCost: input.manaCost,
|
|
1877
|
+
imageUri: input.imageUri,
|
|
1878
|
+
kind: input.kind,
|
|
1879
|
+
turn: input.turn,
|
|
1880
|
+
copies: input.copies,
|
|
1881
|
+
cmc: input.cmc,
|
|
1882
|
+
playedOnCurve: played,
|
|
1883
|
+
notPlayedOnCurve: total - played,
|
|
1884
|
+
totalSimulations: total,
|
|
1885
|
+
pCastOnCurve: pPlay(o),
|
|
1886
|
+
pManaGivenCmc: pManaGivenCmc(o),
|
|
1887
|
+
pInOpeningHand: total === 0 ? 0 : o.inOpeningHand / total,
|
|
1888
|
+
notEnoughLands: total - o.cmc,
|
|
1889
|
+
colorOrTimingFail: o.cmc - o.mana,
|
|
1890
|
+
manaOkUndrawn: o.mana - o.play
|
|
1891
|
+
};
|
|
1892
|
+
}
|
|
1893
|
+
function compareCardsByCmcThenName(a, b) {
|
|
1894
|
+
return a.cmc - b.cmc || a.name.localeCompare(b.name);
|
|
1895
|
+
}
|
|
1896
|
+
function buildObservationsReport(input) {
|
|
1897
|
+
const total = input.totalSimulations;
|
|
1898
|
+
const cards = input.cards.map(cardReportFromInput);
|
|
1899
|
+
cards.sort(compareCardsByCmcThenName);
|
|
1900
|
+
const weakestOnCurve = cards.map((c) => ({
|
|
1901
|
+
name: c.name,
|
|
1902
|
+
pCastOnCurve: c.pCastOnCurve,
|
|
1903
|
+
turn: c.turn
|
|
1904
|
+
})).sort((a, b) => a.pCastOnCurve - b.pCastOnCurve);
|
|
1905
|
+
const colorConstrained = [];
|
|
1906
|
+
const drawDependent = [];
|
|
1907
|
+
for (const inputCard of input.cards) {
|
|
1908
|
+
const o = inputCard.observations;
|
|
1909
|
+
if (o.cmc > 0 && (o.cmc - o.mana) / o.cmc >= COLOR_CONSTRAINED_THRESHOLD) {
|
|
1910
|
+
colorConstrained.push({
|
|
1911
|
+
name: inputCard.name,
|
|
1912
|
+
colorOrTimingFail: o.cmc - o.mana,
|
|
1913
|
+
cmcOpportunities: o.cmc
|
|
1914
|
+
});
|
|
1915
|
+
}
|
|
1916
|
+
if (o.mana > 0 && (o.mana - o.play) / o.mana >= DRAW_DEPENDENT_THRESHOLD) {
|
|
1917
|
+
drawDependent.push({
|
|
1918
|
+
name: inputCard.name,
|
|
1919
|
+
manaOkUndrawn: o.mana - o.play,
|
|
1920
|
+
manaHits: o.mana
|
|
1921
|
+
});
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
colorConstrained.sort(
|
|
1925
|
+
(a, b) => b.colorOrTimingFail / b.cmcOpportunities - a.colorOrTimingFail / a.cmcOpportunities
|
|
1926
|
+
);
|
|
1927
|
+
drawDependent.sort((a, b) => b.manaOkUndrawn / b.manaHits - a.manaOkUndrawn / a.manaHits);
|
|
1928
|
+
return {
|
|
1929
|
+
totalSimulations: total,
|
|
1930
|
+
avgOpeningHandSize: total === 0 ? 0 : input.accumulatedOpeningHandSize / total,
|
|
1931
|
+
avgOpeningLandCount: total === 0 ? 0 : input.accumulatedOpeningHandLandCount / total,
|
|
1932
|
+
deckSize: input.deckSize,
|
|
1933
|
+
deckAverageCmc: input.deckAverageCmc,
|
|
1934
|
+
cards,
|
|
1935
|
+
weakestOnCurve,
|
|
1936
|
+
colorConstrained,
|
|
1937
|
+
drawDependent
|
|
1938
|
+
};
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
// src/run.ts
|
|
1942
|
+
var RunValidationError = class extends Error {
|
|
1943
|
+
constructor(message, options) {
|
|
1944
|
+
super(message, options);
|
|
1945
|
+
this.name = "RunValidationError";
|
|
1946
|
+
}
|
|
1947
|
+
};
|
|
1948
|
+
function emptyLandCounts() {
|
|
1949
|
+
return {
|
|
1950
|
+
totalLandCounts: newManaColorCount(),
|
|
1951
|
+
basicLandCounts: newManaColorCount(),
|
|
1952
|
+
tapLandCounts: newManaColorCount(),
|
|
1953
|
+
checkLandCounts: newManaColorCount(),
|
|
1954
|
+
shockLandCounts: newManaColorCount(),
|
|
1955
|
+
fastLandCounts: newManaColorCount(),
|
|
1956
|
+
slowLandCounts: newManaColorCount(),
|
|
1957
|
+
battleLandCounts: newManaColorCount(),
|
|
1958
|
+
turnLandCounts: newManaColorCount(),
|
|
1959
|
+
surveilLandCounts: newManaColorCount(),
|
|
1960
|
+
bounceLandCounts: newManaColorCount(),
|
|
1961
|
+
triomeLandCounts: newManaColorCount(),
|
|
1962
|
+
cyclingLandCounts: newManaColorCount(),
|
|
1963
|
+
painLandCounts: newManaColorCount(),
|
|
1964
|
+
fetchLandCounts: newManaColorCount(),
|
|
1965
|
+
canopyLandCounts: newManaColorCount(),
|
|
1966
|
+
pathwayLandCounts: newManaColorCount(),
|
|
1967
|
+
otherLandCounts: newManaColorCount(),
|
|
1968
|
+
nonLandCounts: newManaColorCount()
|
|
1969
|
+
};
|
|
1970
|
+
}
|
|
1971
|
+
function emptyOutput() {
|
|
1972
|
+
const report = emptyObservationsReport();
|
|
1973
|
+
return {
|
|
1974
|
+
...report,
|
|
1975
|
+
landCounts: [],
|
|
1976
|
+
...emptyLandCounts()
|
|
1977
|
+
};
|
|
1978
|
+
}
|
|
1979
|
+
function prepareRun(input) {
|
|
1980
|
+
let deck;
|
|
1981
|
+
try {
|
|
1982
|
+
deck = deckFromList(input.code);
|
|
1983
|
+
} catch (e) {
|
|
1984
|
+
const msg = e instanceof DeckcodeError ? e.message : String(e);
|
|
1985
|
+
throw new RunValidationError(`Bad deckcode: ${msg}`, { cause: e });
|
|
1986
|
+
}
|
|
1987
|
+
if (deckIsEmpty(deck)) {
|
|
1988
|
+
throw new RunValidationError("Empty deckcode");
|
|
1989
|
+
}
|
|
1990
|
+
const highestTurn = deckIter(deck).reduce((max2, c) => Math.max(max2, c.card.turn), 0);
|
|
1991
|
+
const startingHandSize = input.startingHandSize ?? 7;
|
|
1992
|
+
const london = londonNever();
|
|
1993
|
+
london.startingHandSize = startingHandSize;
|
|
1994
|
+
london.mulliganDownTo = input.mulliganDownTo;
|
|
1995
|
+
london.mulliganOnLands = new Set(input.mulliganOnLands);
|
|
1996
|
+
const acceptableHashes = [];
|
|
1997
|
+
for (let i = 0; i < input.acceptableHandList.length; i++) {
|
|
1998
|
+
const acceptableHand = input.acceptableHandList[i];
|
|
1999
|
+
const keepCards = /* @__PURE__ */ new Set();
|
|
2000
|
+
for (const cardName of acceptableHand) {
|
|
2001
|
+
const card = ALL_CARDS.cardFromName(cardName);
|
|
2002
|
+
if (!card) {
|
|
2003
|
+
throw new RunValidationError(`Bad card name in acceptableHandList row ${i}: ${cardName}`);
|
|
2004
|
+
}
|
|
2005
|
+
keepCards.add(card.hash);
|
|
2006
|
+
}
|
|
2007
|
+
if (keepCards.size > 0) {
|
|
2008
|
+
london.acceptableHandList.push(keepCards);
|
|
2009
|
+
acceptableHashes.push([...keepCards]);
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
const simConfig = {
|
|
2013
|
+
runCount: input.runs,
|
|
2014
|
+
drawCount: highestTurn,
|
|
2015
|
+
mulligan: asLondonMulligan(london),
|
|
2016
|
+
deck,
|
|
2017
|
+
onThePlay: input.onThePlay,
|
|
2018
|
+
seed: input.seed,
|
|
2019
|
+
epsilon: input.epsilon,
|
|
2020
|
+
parallel: input.parallel,
|
|
2021
|
+
startingHandSize,
|
|
2022
|
+
mulliganDownTo: input.mulliganDownTo,
|
|
2023
|
+
mulliganOnLands: input.mulliganOnLands,
|
|
2024
|
+
acceptableHandList: acceptableHashes
|
|
2025
|
+
};
|
|
2026
|
+
const nonLandCards = deckIter(deck).filter((c) => !isLand(c.card)).map((c) => c.card);
|
|
2027
|
+
return { deck, simConfig, nonLandCards };
|
|
2028
|
+
}
|
|
2029
|
+
function simulationToOutput(deck, sim) {
|
|
2030
|
+
const outputs = emptyOutput();
|
|
2031
|
+
const nonLandWithObs = deckIter(deck).filter((c) => !isLand(c.card)).map((c) => ({
|
|
2032
|
+
name: c.card.name,
|
|
2033
|
+
manaCost: c.card.manaCostString,
|
|
2034
|
+
imageUri: c.card.imageUri,
|
|
2035
|
+
kind: c.card.kind,
|
|
2036
|
+
turn: c.card.turn,
|
|
2037
|
+
copies: c.count,
|
|
2038
|
+
cmc: manaCostCmc(c.card.manaCost),
|
|
2039
|
+
observations: observationsForCardByTurn(sim, c.card, c.card.turn)
|
|
2040
|
+
}));
|
|
2041
|
+
const deckLen = deck.cardCount;
|
|
2042
|
+
const nonLandEntries = deckIter(deck).filter((c) => !isLand(c.card));
|
|
2043
|
+
const nonLandCount = nonLandEntries.reduce((n, c) => n + c.count, 0);
|
|
2044
|
+
const deckAverageCmc = nonLandCount === 0 ? 0 : nonLandEntries.reduce((sum, c) => sum + c.count * manaCostCmc(c.card.manaCost), 0) / nonLandCount;
|
|
2045
|
+
const report = buildObservationsReport({
|
|
2046
|
+
cards: nonLandWithObs,
|
|
2047
|
+
totalSimulations: sim.hands.length,
|
|
2048
|
+
accumulatedOpeningHandSize: sim.accumulatedOpeningHandSize,
|
|
2049
|
+
accumulatedOpeningHandLandCount: sim.accumulatedOpeningHandLandCount,
|
|
2050
|
+
deckSize: deckLen,
|
|
2051
|
+
deckAverageCmc
|
|
2052
|
+
});
|
|
2053
|
+
outputs.totalSimulations = report.totalSimulations;
|
|
2054
|
+
outputs.avgOpeningHandSize = report.avgOpeningHandSize;
|
|
2055
|
+
outputs.avgOpeningLandCount = report.avgOpeningLandCount;
|
|
2056
|
+
outputs.deckSize = report.deckSize;
|
|
2057
|
+
outputs.deckAverageCmc = report.deckAverageCmc;
|
|
2058
|
+
outputs.cards = report.cards;
|
|
2059
|
+
outputs.weakestOnCurve = report.weakestOnCurve;
|
|
2060
|
+
outputs.colorConstrained = report.colorConstrained;
|
|
2061
|
+
outputs.drawDependent = report.drawDependent;
|
|
2062
|
+
outputs.landCounts = deckIter(deck).filter((c) => isLand(c.card)).map((c) => ({
|
|
2063
|
+
name: c.card.name,
|
|
2064
|
+
manaCost: c.card.manaCostString,
|
|
2065
|
+
imageUri: c.card.imageUri,
|
|
2066
|
+
kind: c.card.kind,
|
|
2067
|
+
copies: c.count,
|
|
2068
|
+
hash: c.card.hash
|
|
2069
|
+
}));
|
|
2070
|
+
outputs.landCounts.sort(
|
|
2071
|
+
(a, b) => String(a.kind).localeCompare(String(b.kind)) || a.name.localeCompare(b.name)
|
|
2072
|
+
);
|
|
2073
|
+
for (const cc of deckIter(deck)) {
|
|
2074
|
+
for (let i = 0; i < cc.count; i++) {
|
|
2075
|
+
const card = cc.card;
|
|
2076
|
+
if (isLand(card)) {
|
|
2077
|
+
countManaColor(outputs.totalLandCounts, card.manaCost);
|
|
2078
|
+
}
|
|
2079
|
+
switch (card.kind) {
|
|
2080
|
+
case "BasicLand" /* BasicLand */:
|
|
2081
|
+
countManaColor(outputs.basicLandCounts, card.manaCost);
|
|
2082
|
+
break;
|
|
2083
|
+
case "CheckLand" /* CheckLand */:
|
|
2084
|
+
countManaColor(outputs.checkLandCounts, card.manaCost);
|
|
2085
|
+
break;
|
|
2086
|
+
case "TapLand" /* TapLand */:
|
|
2087
|
+
countManaColor(outputs.tapLandCounts, card.manaCost);
|
|
2088
|
+
break;
|
|
2089
|
+
case "ShockLand" /* ShockLand */:
|
|
2090
|
+
countManaColor(outputs.shockLandCounts, card.manaCost);
|
|
2091
|
+
break;
|
|
2092
|
+
case "FastLand" /* FastLand */:
|
|
2093
|
+
countManaColor(outputs.fastLandCounts, card.manaCost);
|
|
2094
|
+
break;
|
|
2095
|
+
case "SlowLand" /* SlowLand */:
|
|
2096
|
+
countManaColor(outputs.slowLandCounts, card.manaCost);
|
|
2097
|
+
break;
|
|
2098
|
+
case "BattleLand" /* BattleLand */:
|
|
2099
|
+
countManaColor(outputs.battleLandCounts, card.manaCost);
|
|
2100
|
+
break;
|
|
2101
|
+
case "TurnLand" /* TurnLand */:
|
|
2102
|
+
countManaColor(outputs.turnLandCounts, card.manaCost);
|
|
2103
|
+
break;
|
|
2104
|
+
case "SurveilLand" /* SurveilLand */:
|
|
2105
|
+
countManaColor(outputs.surveilLandCounts, card.manaCost);
|
|
2106
|
+
break;
|
|
2107
|
+
case "BounceLand" /* BounceLand */:
|
|
2108
|
+
countManaColor(outputs.bounceLandCounts, card.manaCost);
|
|
2109
|
+
break;
|
|
2110
|
+
case "TriomeLand" /* TriomeLand */:
|
|
2111
|
+
countManaColor(outputs.triomeLandCounts, card.manaCost);
|
|
2112
|
+
break;
|
|
2113
|
+
case "CyclingLand" /* CyclingLand */:
|
|
2114
|
+
countManaColor(outputs.cyclingLandCounts, card.manaCost);
|
|
2115
|
+
break;
|
|
2116
|
+
case "PainLand" /* PainLand */:
|
|
2117
|
+
countManaColor(outputs.painLandCounts, card.manaCost);
|
|
2118
|
+
break;
|
|
2119
|
+
case "FetchLand" /* FetchLand */:
|
|
2120
|
+
countManaColor(outputs.fetchLandCounts, card.manaCost);
|
|
2121
|
+
break;
|
|
2122
|
+
case "CanopyLand" /* CanopyLand */:
|
|
2123
|
+
countManaColor(outputs.canopyLandCounts, card.manaCost);
|
|
2124
|
+
break;
|
|
2125
|
+
case "PathwayLand" /* PathwayLand */:
|
|
2126
|
+
countManaColor(outputs.pathwayLandCounts, card.manaCost);
|
|
2127
|
+
break;
|
|
2128
|
+
case "OtherLand" /* OtherLand */:
|
|
2129
|
+
case "ForcedLand" /* ForcedLand */:
|
|
2130
|
+
countManaColor(outputs.otherLandCounts, card.manaCost);
|
|
2131
|
+
break;
|
|
2132
|
+
default:
|
|
2133
|
+
countManaColor(outputs.nonLandCounts, card.manaCost);
|
|
2134
|
+
break;
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
return outputs;
|
|
2139
|
+
}
|
|
2140
|
+
function run(input) {
|
|
2141
|
+
const { deck, simConfig, nonLandCards } = prepareRun(input);
|
|
2142
|
+
const sim = input.epsilon !== void 0 ? simulationFromConfigAdaptive(simConfig, nonLandCards) : simulationFromConfig(simConfig);
|
|
2143
|
+
return simulationToOutput(deck, sim);
|
|
2144
|
+
}
|
|
2145
|
+
async function runAsync(input) {
|
|
2146
|
+
const { deck, simConfig, nonLandCards } = prepareRun(input);
|
|
2147
|
+
const onProgress = input.onProgress;
|
|
2148
|
+
const signal = input.signal;
|
|
2149
|
+
const sim = await simulationFromConfigAsync(simConfig, {
|
|
2150
|
+
batchSize: input.batchSize,
|
|
2151
|
+
onProgress: onProgress ? (p) => onProgress({ ...p, phase: "simulating" }) : void 0,
|
|
2152
|
+
cards: nonLandCards,
|
|
2153
|
+
signal
|
|
2154
|
+
});
|
|
2155
|
+
signal?.throwIfAborted();
|
|
2156
|
+
if (onProgress) {
|
|
2157
|
+
onProgress({
|
|
2158
|
+
completed: sim.hands.length,
|
|
2159
|
+
total: simConfig.runCount,
|
|
2160
|
+
phase: "scoring"
|
|
2161
|
+
});
|
|
2162
|
+
}
|
|
2163
|
+
if (onProgress !== void 0 || signal !== void 0) {
|
|
2164
|
+
await yieldMacrotask();
|
|
2165
|
+
signal?.throwIfAborted();
|
|
2166
|
+
}
|
|
2167
|
+
return simulationToOutput(deck, sim);
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
// src/scryfall.ts
|
|
2171
|
+
var SPECIAL_LANDS = /* @__PURE__ */ new Map([
|
|
2172
|
+
["Slayers' Stronghold", manaCostFromRgbuwc(0, 0, 0, 0, 0, 1)],
|
|
2173
|
+
["Alchemist's Refuge", manaCostFromRgbuwc(0, 0, 0, 0, 0, 1)],
|
|
2174
|
+
["Desolate Lighthouse", manaCostFromRgbuwc(0, 0, 0, 0, 0, 1)],
|
|
2175
|
+
["Command Tower", manaCostFromRgbuwc(1, 1, 1, 1, 1, 0)],
|
|
2176
|
+
["Opal Palace", manaCostFromRgbuwc(1, 1, 1, 1, 1, 1)],
|
|
2177
|
+
["Path of Ancestry", manaCostFromRgbuwc(1, 1, 1, 1, 1, 0)],
|
|
2178
|
+
["Arid Mesa", manaCostFromRgbuwc(1, 1, 1, 1, 1, 0)],
|
|
2179
|
+
["Bloodstained Mire", manaCostFromRgbuwc(1, 1, 1, 1, 1, 0)],
|
|
2180
|
+
["Flooded Strand", manaCostFromRgbuwc(1, 1, 1, 1, 1, 0)],
|
|
2181
|
+
["Marsh Flats", manaCostFromRgbuwc(1, 1, 1, 1, 1, 0)],
|
|
2182
|
+
["Misty Rainforest", manaCostFromRgbuwc(1, 1, 1, 1, 1, 0)],
|
|
2183
|
+
["Polluted Delta", manaCostFromRgbuwc(1, 1, 1, 1, 1, 0)],
|
|
2184
|
+
["Scalding Tarn", manaCostFromRgbuwc(1, 1, 1, 1, 1, 0)],
|
|
2185
|
+
["Verdant Catacombs", manaCostFromRgbuwc(1, 1, 1, 1, 1, 0)],
|
|
2186
|
+
["Windswept Heath", manaCostFromRgbuwc(1, 1, 1, 1, 1, 0)],
|
|
2187
|
+
["Wooded Foothills", manaCostFromRgbuwc(1, 1, 1, 1, 1, 0)],
|
|
2188
|
+
["Fabled Passage", manaCostFromRgbuwc(1, 1, 1, 1, 1, 0)],
|
|
2189
|
+
["Evolving Wilds", manaCostFromRgbuwc(1, 1, 1, 1, 1, 0)],
|
|
2190
|
+
["Axgard Armory", manaCostFromRgbuwc(0, 0, 0, 0, 1, 0)],
|
|
2191
|
+
["Bretagard Stronghold", manaCostFromRgbuwc(0, 1, 0, 0, 0, 0)],
|
|
2192
|
+
["Gates of Istfell", manaCostFromRgbuwc(0, 0, 0, 0, 1, 0)],
|
|
2193
|
+
["Gnottvold Slumbermound", manaCostFromRgbuwc(1, 0, 0, 0, 0, 0)],
|
|
2194
|
+
["Great Hall of Starnheim", manaCostFromRgbuwc(0, 0, 1, 0, 0, 0)],
|
|
2195
|
+
["Immersturm Skullcairn", manaCostFromRgbuwc(0, 0, 1, 0, 0, 0)],
|
|
2196
|
+
["Littjara Mirrorlake", manaCostFromRgbuwc(0, 0, 0, 1, 0, 0)],
|
|
2197
|
+
["Port of Karfell", manaCostFromRgbuwc(0, 0, 0, 1, 0, 0)],
|
|
2198
|
+
["Skemfar Elderhall", manaCostFromRgbuwc(0, 1, 0, 0, 0, 0)],
|
|
2199
|
+
["Surtland Frostpyre", manaCostFromRgbuwc(1, 0, 0, 0, 0, 0)],
|
|
2200
|
+
["Terramorphic Expanse", manaCostFromRgbuwc(1, 1, 1, 1, 1, 0)],
|
|
2201
|
+
["Prismatic Vista", manaCostFromRgbuwc(1, 1, 1, 1, 1, 0)],
|
|
2202
|
+
["Barkchannel Pathway // Tidechannel Pathway", manaCostFromRgbuwc(0, 1, 0, 1, 0, 0)],
|
|
2203
|
+
["Barkchannel Pathway", manaCostFromRgbuwc(0, 1, 0, 0, 0, 0)],
|
|
2204
|
+
["Tidechannel Pathway", manaCostFromRgbuwc(0, 0, 0, 1, 0, 0)],
|
|
2205
|
+
["Blightstep Pathway // Searstep Pathway", manaCostFromRgbuwc(1, 0, 1, 0, 0, 0)],
|
|
2206
|
+
["Blightstep Pathway", manaCostFromRgbuwc(0, 0, 1, 0, 0, 0)],
|
|
2207
|
+
// Matches Rust SPECIAL_LANDS (historical quirk: U instead of R)
|
|
2208
|
+
["Searstep Pathway", manaCostFromRgbuwc(0, 0, 0, 1, 0, 0)],
|
|
2209
|
+
["Branchloft Pathway // Boulderloft Pathway", manaCostFromRgbuwc(0, 1, 0, 0, 1, 0)],
|
|
2210
|
+
["Branchloft Pathway", manaCostFromRgbuwc(0, 1, 0, 0, 0, 0)],
|
|
2211
|
+
["Boulderloft Pathway", manaCostFromRgbuwc(0, 0, 0, 0, 1, 0)],
|
|
2212
|
+
["Brightclimb Pathway // Grimclimb Pathway", manaCostFromRgbuwc(0, 0, 1, 0, 1, 0)],
|
|
2213
|
+
["Brightclimb Pathway", manaCostFromRgbuwc(0, 0, 0, 0, 1, 0)],
|
|
2214
|
+
["Grimclimb Pathway", manaCostFromRgbuwc(0, 0, 1, 0, 0, 0)],
|
|
2215
|
+
["Clearwater Pathway // Murkwater Pathway", manaCostFromRgbuwc(0, 0, 1, 1, 0, 0)],
|
|
2216
|
+
["Clearwater Pathway", manaCostFromRgbuwc(0, 0, 0, 1, 0, 0)],
|
|
2217
|
+
["Murkwater Pathway", manaCostFromRgbuwc(0, 0, 1, 0, 0, 0)],
|
|
2218
|
+
["Cragcrown Pathway // Timbercrown Pathway", manaCostFromRgbuwc(1, 1, 0, 0, 0, 0)],
|
|
2219
|
+
["Cragcrown Pathway", manaCostFromRgbuwc(1, 0, 0, 0, 0, 0)],
|
|
2220
|
+
["Timbercrown Pathway", manaCostFromRgbuwc(0, 1, 0, 0, 0, 0)],
|
|
2221
|
+
["Darkbore Pathway // Slitherbore Pathway", manaCostFromRgbuwc(0, 1, 1, 0, 0, 0)],
|
|
2222
|
+
["Darkbore Pathway", manaCostFromRgbuwc(0, 0, 1, 0, 0, 0)],
|
|
2223
|
+
["Slitherbore Pathway", manaCostFromRgbuwc(0, 1, 0, 0, 0, 0)],
|
|
2224
|
+
["Hengegate Pathway // Mistgate Pathway", manaCostFromRgbuwc(0, 0, 0, 1, 1, 0)],
|
|
2225
|
+
["Hengegate Pathway", manaCostFromRgbuwc(0, 0, 0, 0, 1, 0)],
|
|
2226
|
+
["Mistgate Pathway", manaCostFromRgbuwc(0, 0, 0, 1, 0, 0)],
|
|
2227
|
+
["Needleverge Pathway // Pillarverge Pathway", manaCostFromRgbuwc(0, 0, 0, 0, 1, 0)],
|
|
2228
|
+
["Needleverge Pathway", manaCostFromRgbuwc(1, 0, 0, 0, 0, 0)],
|
|
2229
|
+
["Pillarverge Pathway", manaCostFromRgbuwc(0, 0, 0, 0, 1, 0)],
|
|
2230
|
+
["Riverglide Pathway // Lavaglide Pathway", manaCostFromRgbuwc(1, 0, 0, 1, 0, 0)],
|
|
2231
|
+
["Riverglide Pathway", manaCostFromRgbuwc(0, 0, 0, 1, 0, 0)],
|
|
2232
|
+
["Lavaglide Pathway", manaCostFromRgbuwc(1, 0, 0, 0, 0, 0)],
|
|
2233
|
+
["Kor Haven", manaCostFromRgbuwc(0, 0, 0, 0, 0, 1)],
|
|
2234
|
+
// Chooser / tribal lands: "Add one mana of any color. Spend this mana only to cast
|
|
2235
|
+
// [a spell of a chosen/specific type]..." — modeled as rainbow, assuming the player
|
|
2236
|
+
// always chooses a type/restriction that matches the goal spell (same approximation as Command Tower).
|
|
2237
|
+
["Cavern of Souls", manaCostFromRgbuwc(1, 1, 1, 1, 1, 1)],
|
|
2238
|
+
["Secluded Courtyard", manaCostFromRgbuwc(1, 1, 1, 1, 1, 1)],
|
|
2239
|
+
["Unclaimed Territory", manaCostFromRgbuwc(1, 1, 1, 1, 1, 1)],
|
|
2240
|
+
["Ancient Ziggurat", manaCostFromRgbuwc(1, 1, 1, 1, 1, 0)],
|
|
2241
|
+
["Pillar of the Paruns", manaCostFromRgbuwc(1, 1, 1, 1, 1, 0)],
|
|
2242
|
+
["Abundant Countryside", manaCostFromRgbuwc(1, 1, 1, 1, 1, 1)],
|
|
2243
|
+
["Ally Encampment", manaCostFromRgbuwc(1, 1, 1, 1, 1, 1)],
|
|
2244
|
+
["A-Base Camp", manaCostFromRgbuwc(1, 1, 1, 1, 1, 1)],
|
|
2245
|
+
["Avengers Tower", manaCostFromRgbuwc(1, 1, 1, 1, 1, 1)],
|
|
2246
|
+
["Base Camp", manaCostFromRgbuwc(1, 1, 1, 1, 1, 1)],
|
|
2247
|
+
["Brotherhood Headquarters", manaCostFromRgbuwc(1, 1, 1, 1, 1, 1)],
|
|
2248
|
+
["Bucolic Ranch", manaCostFromRgbuwc(1, 1, 1, 1, 1, 1)],
|
|
2249
|
+
["Castle Doom", manaCostFromRgbuwc(1, 1, 1, 1, 1, 1)],
|
|
2250
|
+
["Corrupted Crossroads", manaCostFromRgbuwc(1, 1, 1, 1, 1, 1)],
|
|
2251
|
+
["Echoing Cavern", manaCostFromRgbuwc(1, 1, 1, 1, 1, 1)],
|
|
2252
|
+
["Eclipsed Realms", manaCostFromRgbuwc(1, 1, 1, 1, 1, 1)],
|
|
2253
|
+
["Gallifrey Council Chamber", manaCostFromRgbuwc(1, 1, 1, 1, 1, 1)],
|
|
2254
|
+
["Great Hall of the Biblioplex", manaCostFromRgbuwc(1, 1, 1, 1, 1, 1)],
|
|
2255
|
+
["Haven of the Spirit Dragon", manaCostFromRgbuwc(1, 1, 1, 1, 1, 1)],
|
|
2256
|
+
["Jasmine Dragon Tea Shop", manaCostFromRgbuwc(1, 1, 1, 1, 1, 1)],
|
|
2257
|
+
["Maelstrom of the Spirit Dragon", manaCostFromRgbuwc(1, 1, 1, 1, 1, 1)],
|
|
2258
|
+
["Mech Hangar", manaCostFromRgbuwc(1, 1, 1, 1, 1, 1)],
|
|
2259
|
+
["Plaza of Heroes", manaCostFromRgbuwc(1, 1, 1, 1, 1, 1)],
|
|
2260
|
+
["Power Depot", manaCostFromRgbuwc(1, 1, 1, 1, 1, 1)],
|
|
2261
|
+
["Primal Beyond", manaCostFromRgbuwc(1, 1, 1, 1, 1, 1)],
|
|
2262
|
+
["Sliver Hive", manaCostFromRgbuwc(1, 1, 1, 1, 1, 1)],
|
|
2263
|
+
["The Seedcore", manaCostFromRgbuwc(1, 1, 1, 1, 1, 1)],
|
|
2264
|
+
["Turtle Lair", manaCostFromRgbuwc(1, 1, 1, 1, 1, 1)],
|
|
2265
|
+
["Villainous Hideout", manaCostFromRgbuwc(1, 1, 1, 1, 1, 1)],
|
|
2266
|
+
["Voldaren Estate", manaCostFromRgbuwc(1, 1, 1, 1, 1, 1)],
|
|
2267
|
+
["White Lotus Hideout", manaCostFromRgbuwc(1, 1, 1, 1, 1, 1)],
|
|
2268
|
+
["Second City", manaCostFromRgbuwc(1, 1, 1, 1, 1, 1)],
|
|
2269
|
+
["Secret Base", manaCostFromRgbuwc(1, 1, 1, 1, 1, 1)],
|
|
2270
|
+
["Tarkir Omenpath", manaCostFromRgbuwc(1, 1, 1, 1, 1, 1)],
|
|
2271
|
+
["Underdome", manaCostFromRgbuwc(1, 1, 1, 1, 1, 1)],
|
|
2272
|
+
// Lotus Field: "Add three mana of any one color." Modeled as WUBRG + manaPerTap=3,
|
|
2273
|
+
// which overestimates multicolor costs (matcher can spend the 3 units as different colors).
|
|
2274
|
+
["Lotus Field", manaCostFromRgbuwc(1, 1, 1, 1, 1, 0)]
|
|
2275
|
+
]);
|
|
2276
|
+
var MULTI_MANA_LANDS = /* @__PURE__ */ new Map([
|
|
2277
|
+
["Ancient Tomb", 2],
|
|
2278
|
+
["Lotus Field", 3]
|
|
2279
|
+
]);
|
|
2280
|
+
function landKindFromOracleText(oracleText, typeLine, name = "") {
|
|
2281
|
+
const text = normalizeOracleForEtb(oracleText);
|
|
2282
|
+
const isShock = text.includes("you may pay 2 life") && text.includes("enters tapped");
|
|
2283
|
+
const isCheck = text.includes("enters tapped unless you control a");
|
|
2284
|
+
const isFast = text.includes("two or fewer other lands");
|
|
2285
|
+
const isSlow = text.includes("two or more other lands");
|
|
2286
|
+
const isBattle = text.includes("two or more basic lands");
|
|
2287
|
+
const isTurn = text.includes("first, second, or third turn");
|
|
2288
|
+
const isBounce = text.includes("enters tapped") && text.includes("return a land you control");
|
|
2289
|
+
const isSurveil = text.includes("enters tapped") && text.includes("surveil");
|
|
2290
|
+
const isTriome = text.includes("enters tapped") && basicLandTypeCount(typeLine) >= 3;
|
|
2291
|
+
const isCycling = text.includes("enters tapped") && text.includes("cycling");
|
|
2292
|
+
const isTap = text.includes("enters tapped");
|
|
2293
|
+
const isFetch = text.includes("search your library") && (text.includes("sacrifice") || text.includes("sac ")) && (text.includes("basic land") || text.includes("plains") || text.includes("island") || text.includes("swamp") || text.includes("mountain") || text.includes("forest"));
|
|
2294
|
+
const isCanopy = text.includes("draw a card") && (text.includes("sacrifice this land") || text.includes("sacrifice ~"));
|
|
2295
|
+
const isPain = text.includes("deals 1 damage to you") && !text.includes("enters tapped") && !isCanopy;
|
|
2296
|
+
const isPathway = name.toLowerCase().includes("pathway");
|
|
2297
|
+
const isBasic = typeLine.includes("Basic Land");
|
|
2298
|
+
if (isShock) return "ShockLand" /* ShockLand */;
|
|
2299
|
+
if (isCheck) return "CheckLand" /* CheckLand */;
|
|
2300
|
+
if (isFast) return "FastLand" /* FastLand */;
|
|
2301
|
+
if (isSlow) return "SlowLand" /* SlowLand */;
|
|
2302
|
+
if (isBattle) return "BattleLand" /* BattleLand */;
|
|
2303
|
+
if (isTurn) return "TurnLand" /* TurnLand */;
|
|
2304
|
+
if (isBounce) return "BounceLand" /* BounceLand */;
|
|
2305
|
+
if (isSurveil) return "SurveilLand" /* SurveilLand */;
|
|
2306
|
+
if (isTriome) return "TriomeLand" /* TriomeLand */;
|
|
2307
|
+
if (isCycling) return "CyclingLand" /* CyclingLand */;
|
|
2308
|
+
if (isTap) return "TapLand" /* TapLand */;
|
|
2309
|
+
if (isFetch) return "FetchLand" /* FetchLand */;
|
|
2310
|
+
if (isCanopy) return "CanopyLand" /* CanopyLand */;
|
|
2311
|
+
if (isPain) return "PainLand" /* PainLand */;
|
|
2312
|
+
if (isPathway) return "PathwayLand" /* PathwayLand */;
|
|
2313
|
+
if (isBasic) return "BasicLand" /* BasicLand */;
|
|
2314
|
+
return "OtherLand" /* OtherLand */;
|
|
2315
|
+
}
|
|
2316
|
+
function etbClauseFromOracle(oracleText) {
|
|
2317
|
+
const text = normalizeOracleForEtb(oracleText);
|
|
2318
|
+
const idx = text.indexOf("enters tapped");
|
|
2319
|
+
if (idx === -1) return null;
|
|
2320
|
+
let clause = text.slice(idx);
|
|
2321
|
+
const end = clause.search(/[.\n]/);
|
|
2322
|
+
if (end !== -1) clause = clause.slice(0, end + 1);
|
|
2323
|
+
return clause.trim();
|
|
2324
|
+
}
|
|
2325
|
+
function colorLetter(color) {
|
|
2326
|
+
switch (color) {
|
|
2327
|
+
case 0 /* Red */:
|
|
2328
|
+
return "R";
|
|
2329
|
+
case 1 /* Green */:
|
|
2330
|
+
return "G";
|
|
2331
|
+
case 2 /* Black */:
|
|
2332
|
+
return "B";
|
|
2333
|
+
case 3 /* Blue */:
|
|
2334
|
+
return "U";
|
|
2335
|
+
case 4 /* White */:
|
|
2336
|
+
return "W";
|
|
2337
|
+
default:
|
|
2338
|
+
return "";
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
function isColor01(colorIdentity, oracleText, color) {
|
|
2342
|
+
const letter = colorLetter(color);
|
|
2343
|
+
if (letter !== "" && colorIdentity.has(letter) || color === 5 /* Colorless */ && colorIdentity.size === 0 || oracleText.includes("Add one mana of any color.") && !oracleText.includes("Add one mana of any color. Spend this mana only")) {
|
|
2344
|
+
return 1;
|
|
2345
|
+
}
|
|
2346
|
+
return 0;
|
|
2347
|
+
}
|
|
2348
|
+
function resolveImageUri(card) {
|
|
2349
|
+
const normal = card.image_uris?.["normal"];
|
|
2350
|
+
if (normal) return normal;
|
|
2351
|
+
return card.card_faces?.[0]?.image_uris?.["normal"] ?? "";
|
|
2352
|
+
}
|
|
2353
|
+
function scryfallCardToCard(raw) {
|
|
2354
|
+
const name = raw.name ?? "";
|
|
2355
|
+
const oracleText = raw.oracle_text ?? "";
|
|
2356
|
+
const typeLine = raw.type_line ?? "";
|
|
2357
|
+
const manaCostStr = raw.mana_cost ?? "";
|
|
2358
|
+
const colorIdentity = new Set(raw.color_identity ?? []);
|
|
2359
|
+
const isLand2 = typeLine.includes("Land");
|
|
2360
|
+
let kind;
|
|
2361
|
+
let manaCost;
|
|
2362
|
+
let allManaCosts;
|
|
2363
|
+
let basicLandTypes = 0;
|
|
2364
|
+
let checkTypes = 0;
|
|
2365
|
+
let manaPerTap = 1;
|
|
2366
|
+
if (isLand2) {
|
|
2367
|
+
const special = SPECIAL_LANDS.get(name);
|
|
2368
|
+
manaCost = special ?? manaCostFromRgbuwc(
|
|
2369
|
+
isColor01(colorIdentity, oracleText, 0 /* Red */),
|
|
2370
|
+
isColor01(colorIdentity, oracleText, 1 /* Green */),
|
|
2371
|
+
isColor01(colorIdentity, oracleText, 2 /* Black */),
|
|
2372
|
+
isColor01(colorIdentity, oracleText, 3 /* Blue */),
|
|
2373
|
+
isColor01(colorIdentity, oracleText, 4 /* White */),
|
|
2374
|
+
isColor01(colorIdentity, oracleText, 5 /* Colorless */)
|
|
2375
|
+
);
|
|
2376
|
+
kind = landKindFromOracleText(oracleText, typeLine, name);
|
|
2377
|
+
allManaCosts = [manaCost];
|
|
2378
|
+
basicLandTypes = basicLandTypesFromTypeLine(typeLine);
|
|
2379
|
+
checkTypes = kind === "CheckLand" /* CheckLand */ ? checkTypesFromOracleText(oracleText) : 0;
|
|
2380
|
+
manaPerTap = MULTI_MANA_LANDS.get(name) ?? 1;
|
|
2381
|
+
} else {
|
|
2382
|
+
kind = "Unknown" /* Unknown */;
|
|
2383
|
+
allManaCosts = manaCostsFromStr(manaCostStr);
|
|
2384
|
+
const first = allManaCosts[0] ?? emptyManaCost();
|
|
2385
|
+
manaCost = manaCostFromRgbuwc(first.r, first.g, first.b, first.u, first.w, first.c);
|
|
2386
|
+
}
|
|
2387
|
+
const turn = Math.max(1, manaCostCmc(manaCost));
|
|
2388
|
+
const set = (raw.set ?? "").toLowerCase();
|
|
2389
|
+
return {
|
|
2390
|
+
name,
|
|
2391
|
+
oracleId: raw.oracle_id ?? "",
|
|
2392
|
+
manaCostString: manaCostStr,
|
|
2393
|
+
imageUri: resolveImageUri(raw),
|
|
2394
|
+
kind,
|
|
2395
|
+
hash: hashCardName(name),
|
|
2396
|
+
turn,
|
|
2397
|
+
manaCost,
|
|
2398
|
+
allManaCosts,
|
|
2399
|
+
arenaId: raw.arena_id ?? 0,
|
|
2400
|
+
rarity: parseRarity(raw.rarity ?? ""),
|
|
2401
|
+
set,
|
|
2402
|
+
isFace: raw.object === "card_face",
|
|
2403
|
+
basicLandTypes,
|
|
2404
|
+
checkTypes,
|
|
2405
|
+
manaPerTap
|
|
2406
|
+
};
|
|
2407
|
+
}
|
|
2408
|
+
function isLegalInSomeFormat(card) {
|
|
2409
|
+
const values = Object.values(card.legalities ?? {});
|
|
2410
|
+
return !values.every((l) => l === "not_legal");
|
|
2411
|
+
}
|
|
2412
|
+
function flattenScryfallCardFaces(cards) {
|
|
2413
|
+
const out = [...cards];
|
|
2414
|
+
for (const card of cards) {
|
|
2415
|
+
for (const face of card.card_faces ?? []) {
|
|
2416
|
+
const flattened = { ...face };
|
|
2417
|
+
const faceImages = face.image_uris ?? {};
|
|
2418
|
+
if (Object.keys(faceImages).length === 0) {
|
|
2419
|
+
flattened.image_uris = card.image_uris ? { ...card.image_uris } : {};
|
|
2420
|
+
}
|
|
2421
|
+
flattened.set = card.set;
|
|
2422
|
+
flattened.oracle_id = card.oracle_id;
|
|
2423
|
+
flattened.id = card.id;
|
|
2424
|
+
flattened.rarity = card.rarity;
|
|
2425
|
+
flattened.collector_number = card.collector_number;
|
|
2426
|
+
flattened.object = "card_face";
|
|
2427
|
+
out.push(flattened);
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
return out;
|
|
2431
|
+
}
|
|
2432
|
+
function scryfallCardsToCards(cards) {
|
|
2433
|
+
const filtered = cards.filter(isLegalInSomeFormat);
|
|
2434
|
+
const flattened = flattenScryfallCardFaces(filtered);
|
|
2435
|
+
return flattened.map(scryfallCardToCard);
|
|
2436
|
+
}
|
|
2437
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
2438
|
+
0 && (module.exports = {
|
|
2439
|
+
ALL_CARDS,
|
|
2440
|
+
BasicLandType,
|
|
2441
|
+
COLOR_CONSTRAINED_THRESHOLD,
|
|
2442
|
+
CardKind,
|
|
2443
|
+
DRAW_DEPENDENT_THRESHOLD,
|
|
2444
|
+
DeckcodeError,
|
|
2445
|
+
PlayOrder,
|
|
2446
|
+
Rarity,
|
|
2447
|
+
RunValidationError,
|
|
2448
|
+
SPECIAL_LANDS,
|
|
2449
|
+
asLondonMulligan,
|
|
2450
|
+
asNeverMulligan,
|
|
2451
|
+
autoTapByTurn,
|
|
2452
|
+
availableTurnForLand,
|
|
2453
|
+
basicLandTypesFromTypeLine,
|
|
2454
|
+
checkTypesFromOracleText,
|
|
2455
|
+
createMulberry32,
|
|
2456
|
+
deckFlatten,
|
|
2457
|
+
deckFromCards,
|
|
2458
|
+
deckFromList,
|
|
2459
|
+
decklist,
|
|
2460
|
+
drawCmcAutoTap,
|
|
2461
|
+
etbClauseFromOracle,
|
|
2462
|
+
getAllCards,
|
|
2463
|
+
handFromOpeningAndDraws,
|
|
2464
|
+
hashCardName,
|
|
2465
|
+
isLand,
|
|
2466
|
+
isLandKind,
|
|
2467
|
+
landKindFromOracleText,
|
|
2468
|
+
loadCardsFromGzipBytes,
|
|
2469
|
+
londonNever,
|
|
2470
|
+
manaCostCmc,
|
|
2471
|
+
manaCostFromRgbuwc,
|
|
2472
|
+
manaCostsFromStr,
|
|
2473
|
+
observationsForCard,
|
|
2474
|
+
pMana,
|
|
2475
|
+
pManaGivenCmc,
|
|
2476
|
+
pPlay,
|
|
2477
|
+
parseDecklist,
|
|
2478
|
+
playCmcAutoTap,
|
|
2479
|
+
run,
|
|
2480
|
+
runAsync,
|
|
2481
|
+
scryfallCardToCard,
|
|
2482
|
+
scryfallCardsToCards,
|
|
2483
|
+
setAllCards,
|
|
2484
|
+
simulationFromConfig,
|
|
2485
|
+
simulationFromConfigAsync,
|
|
2486
|
+
wilsonHalfWidth
|
|
2487
|
+
});
|