]> gitweb.factorcode.org Git - factor.git/blob - extra/scryfall/scryfall.factor
interpolate: split out format into a hook
[factor.git] / extra / scryfall / scryfall.factor
1 ! Copyright (C) 2024 Doug Coleman.
2 ! See https://factorcode.org/license.txt for BSD license.
3 USING: accessors arrays assocs assocs.extras calendar
4 calendar.parser combinators combinators.short-circuit
5 combinators.smart formatting grouping http.download
6 images.loader images.viewer io io.directories json json.http
7 kernel math math.combinatorics math.order math.parser
8 math.statistics namespaces random sequences sequences.deep
9 sequences.extras sequences.generalizations sets sorting
10 sorting.specification splitting splitting.extras strings
11 ui.gadgets.panes unicode urls ;
12 IN: scryfall
13
14 CONSTANT: scryfall-oracle-json-path "resource:scryfall-oracle-json"
15 CONSTANT: scryfall-artwork-json-path "resource:scryfall-artwork-json"
16 CONSTANT: scryfall-default-json-path "resource:scryfall-default-json"
17 CONSTANT: scryfall-all-json-path "resource:scryfall-all-json"
18 CONSTANT: scryfall-rulings-json-path "resource:scryfall-rulings-json"
19 CONSTANT: scryfall-images-path "resource:scryfall-images/"
20
21 : ?write ( str/f -- ) [ write ] when* ;
22 : ?print ( str/f -- ) [ print ] [ nl ] if* ;
23
24 : download-scryfall-bulk-json ( -- json )
25     "https://api.scryfall.com/bulk-data" http-get-json nip ;
26
27 : find-scryfall-json ( type -- json/f )
28     [ download-scryfall-bulk-json "data" of ] dip '[ "type" of _ = ] filter ?first ;
29
30 : load-scryfall-json ( type path -- uri )
31     [ find-scryfall-json "download_uri" of ] dip
32     10 days download-outdated-as path>json ;
33
34 MEMO: mtg-oracle-cards ( -- json )
35     "oracle_cards" scryfall-oracle-json-path load-scryfall-json ;
36
37 MEMO: mtg-artwork-cards ( -- json )
38     "unique_artwork" scryfall-artwork-json-path load-scryfall-json ;
39
40 MEMO: scryfall-default-cards-json ( -- json )
41     "default_cards" scryfall-default-json-path load-scryfall-json ;
42
43 MEMO: scryfall-all-cards-json ( -- json )
44     "all_cards" scryfall-all-json-path load-scryfall-json ;
45
46 MEMO: scryfall-rulings-json ( -- json )
47     "rulings" scryfall-rulings-json-path load-scryfall-json ;
48
49 : ensure-scryfall-images-directory ( -- )
50     scryfall-images-path make-directories ;
51
52 : scryfall-local-image-path ( string -- path )
53     >url path>> "/" ?head drop "/" "-" replace
54     scryfall-images-path "" prepend-as ;
55
56 : filter-multi-card-faces ( assoc -- seq )
57     [ "card_faces" of length 1 > ] filter ; inline
58
59 : multi-card-faces? ( assoc -- seq )
60     "card_faces" of length 1 > ; inline
61
62 : card>image-uris ( assoc -- seq )
63     [ "image_uris" of ]
64     [ 1array ]
65     [ "card_faces" of [ "image_uris" of ] map ] ?if ;
66
67 : small-images ( seq -- seq' ) [ "small" of ] map ;
68 : normal-images ( seq -- seq' ) [ "normal" of ] map ;
69
70 : download-scryfall-image ( assoc -- path )
71     dup scryfall-local-image-path dup delete-when-zero-size
72     [ download-once-as ] [ nip ] if ;
73
74 : download-normal-images ( seq -- seq' )
75     ensure-scryfall-images-directory
76     normal-images [ download-scryfall-image load-image ] map ;
77
78 : download-small-images ( seq -- seq' )
79     ensure-scryfall-images-directory
80     small-images [ download-scryfall-image load-image ] map ;
81
82 : collect-cards-by-name ( seq -- assoc ) [ "name" of ] collect-by ;
83 : collect-cards-by-cmc ( seq -- assoc ) [ "cmc" of ] collect-by ;
84 : collect-cards-by-mana-cost ( seq -- assoc ) [ "mana_cost" of ] collect-by ;
85 : collect-cards-by-color-identity ( seq -- assoc ) [ "color_identity" of ] collect-by-multi ;
86 : red-color-identity ( seq -- seq' ) collect-cards-by-color-identity "R" of ;
87 : blue-color-identity ( seq -- seq' ) collect-cards-by-color-identity "U" of ;
88 : green-color-identity ( seq -- seq' ) collect-cards-by-color-identity "G" of ;
89 : black-color-identity ( seq -- seq' ) collect-cards-by-color-identity "B" of ;
90 : white-color-identity ( seq -- seq' ) collect-cards-by-color-identity "W" of ;
91
92 : find-card-by-color-identity-intersect ( cards colors -- cards' )
93     [ collect-cards-by-color-identity ] dip [ of ] with map intersect-all ;
94
95 : find-any-color-identities ( cards colors -- cards' )
96     [ collect-cards-by-color-identity ] dip [ of ] with map union-all ;
97
98 : color-identity-complement ( seq -- seq' ) [ { "B" "G" "R" "U" "W" } ] dip diff ;
99
100 : split-mana-cost ( string -- seq )
101     f like [ " // " split1 swap ] { } loop>sequence nip ;
102
103 : casting-cost-combinations ( seq -- seq' )
104     sequence-cartesian-product [ [ first ] sort-by ] map ;
105
106 : parse-mana-cost ( string -- seq )
107     split-mana-cost
108     [
109         "{}" split harvest
110         [ "/" split ] map
111         casting-cost-combinations
112     ] map ;
113
114 : remove-color-identities ( cards colors -- cards' )
115     dupd find-any-color-identities diff ;
116
117 : remove-other-color-identities ( cards colors -- cards' )
118     color-identity-complement remove-color-identities ;
119
120 : find-only-color-identities ( cards colors -- cards' )
121     [ find-any-color-identities ] keep remove-other-color-identities ;
122
123 : filter-color-identity-length= ( seq n -- seq' ) '[ "color_identity" of length _ = ] filter ;
124 : filter-color-identity-length<= ( seq n -- seq' ) '[ "color_identity" of length _ <= ] filter ;
125 : find-exact-color-identities ( cards seq -- cards' )
126     [ find-card-by-color-identity-intersect ] keep length filter-color-identity-length= ;
127
128 : filter-azorius-any ( seq -- seq' ) { "W" "U" } find-any-color-identities ;
129 : filter-dimir-any ( seq -- seq' ) { "U" "B" } find-any-color-identities ;
130 : filter-orzhov-any ( seq -- seq' ) { "W" "B" } find-any-color-identities ;
131 : filter-boros-any ( seq -- seq' ) { "R" "W" } find-any-color-identities ;
132 : filter-selesnya-any ( seq -- seq' ) { "G" "W" } find-any-color-identities ;
133 : filter-simic-any ( seq -- seq' ) { "G" "U" } find-any-color-identities ;
134 : filter-izzet-any ( seq -- seq' ) { "R" "U" } find-any-color-identities ;
135 : filter-golgari-any ( seq -- seq' ) { "B" "G" } find-any-color-identities ;
136 : filter-rakdos-any ( seq -- seq' ) { "B" "R" } find-any-color-identities ;
137 : filter-gruul-any ( seq -- seq' ) { "G" "R" } find-any-color-identities ;
138
139 : filter-azorius-only ( seq -- seq' ) { "W" "U" } find-only-color-identities ;
140 : filter-dimir-only ( seq -- seq' ) { "U" "B" } find-only-color-identities ;
141 : filter-orzhov-only ( seq -- seq' ) { "W" "B" } find-only-color-identities ;
142 : filter-boros-only ( seq -- seq' ) { "R" "W" } find-only-color-identities ;
143 : filter-selesnya-only ( seq -- seq' ) { "G" "W" } find-only-color-identities ;
144 : filter-simic-only ( seq -- seq' ) { "G" "U" } find-only-color-identities ;
145 : filter-izzet-only ( seq -- seq' ) { "R" "U" } find-only-color-identities ;
146 : filter-golgari-only ( seq -- seq' ) { "B" "G" } find-only-color-identities ;
147 : filter-rakdos-only ( seq -- seq' ) { "B" "R" } find-only-color-identities ;
148 : filter-gruul-only ( seq -- seq' ) { "G" "R" } find-only-color-identities ;
149
150 : filter-azorius-exact ( seq -- seq' ) { "W" "U" } find-exact-color-identities ;
151 : filter-dimir-exact ( seq -- seq' ) { "U" "B" } find-exact-color-identities ;
152 : filter-orzhov-exact ( seq -- seq' ) { "W" "B" } find-exact-color-identities ;
153 : filter-boros-exact ( seq -- seq' ) { "R" "W" } find-exact-color-identities ;
154 : filter-selesnya-exact ( seq -- seq' ) { "G" "W" } find-exact-color-identities ;
155 : filter-simic-exact ( seq -- seq' ) { "G" "U" } find-exact-color-identities ;
156 : filter-izzet-exact ( seq -- seq' ) { "R" "U" } find-exact-color-identities ;
157 : filter-golgari-exact ( seq -- seq' ) { "B" "G" } find-exact-color-identities ;
158 : filter-rakdos-exact ( seq -- seq' ) { "B" "R" } find-exact-color-identities ;
159 : filter-gruul-exact ( seq -- seq' ) { "G" "R" } find-exact-color-identities ;
160
161 : filter-bant-any ( seq -- seq' ) { "G" "W" "U" } find-any-color-identities ;
162 : filter-esper-any ( seq -- seq' ) { "W" "U" "B" } find-any-color-identities ;
163 : filter-grixis-any ( seq -- seq' ) { "U" "B" "R" } find-any-color-identities ;
164 : filter-jund-any ( seq -- seq' ) { "B" "R" "G" } find-any-color-identities ;
165 : filter-naya-any ( seq -- seq' ) { "R" "G" "W" } find-any-color-identities ;
166 : filter-abzan-any ( seq -- seq' ) { "W" "B" "G" } find-any-color-identities ;
167 : filter-jeskai-any ( seq -- seq' ) { "U" "R" "W" } find-any-color-identities ;
168 : filter-mardu-any ( seq -- seq' ) { "R" "W" "B" } find-any-color-identities ;
169 : filter-sultai-any ( seq -- seq' ) { "B" "G" "U" } find-any-color-identities ;
170 : filter-temur-any ( seq -- seq' ) { "G" "U" "R" } find-any-color-identities ;
171
172 : filter-bant-only ( seq -- seq' ) { "G" "W" "U" } find-only-color-identities ;
173 : filter-esper-only ( seq -- seq' ) { "W" "U" "B" } find-only-color-identities ;
174 : filter-grixis-only ( seq -- seq' ) { "U" "B" "R" } find-only-color-identities ;
175 : filter-jund-only ( seq -- seq' ) { "B" "R" "G" } find-only-color-identities ;
176 : filter-naya-only ( seq -- seq' ) { "R" "G" "W" } find-only-color-identities ;
177 : filter-abzan-only ( seq -- seq' ) { "W" "B" "G" } find-only-color-identities ;
178 : filter-jeskai-only ( seq -- seq' ) { "U" "R" "W" } find-only-color-identities ;
179 : filter-mardu-only ( seq -- seq' ) { "R" "W" "B" } find-only-color-identities ;
180 : filter-sultai-only ( seq -- seq' ) { "B" "G" "U" } find-only-color-identities ;
181 : filter-temur-only ( seq -- seq' ) { "G" "U" "R" } find-only-color-identities ;
182
183 : filter-bant-exact ( seq -- seq' ) { "G" "W" "U" } find-exact-color-identities ;
184 : filter-esper-exact ( seq -- seq' ) { "W" "U" "B" } find-exact-color-identities ;
185 : filter-grixis-exact ( seq -- seq' ) { "U" "B" "R" } find-exact-color-identities ;
186 : filter-jund-exact ( seq -- seq' ) { "B" "R" "G" } find-exact-color-identities ;
187 : filter-naya-exact ( seq -- seq' ) { "R" "G" "W" } find-exact-color-identities ;
188 : filter-abzan-exact ( seq -- seq' ) { "W" "B" "G" } find-exact-color-identities ;
189 : filter-jeskai-exact ( seq -- seq' ) { "U" "R" "W" } find-exact-color-identities ;
190 : filter-mardu-exact ( seq -- seq' ) { "R" "W" "B" } find-exact-color-identities ;
191 : filter-sultai-exact ( seq -- seq' ) { "B" "G" "U" } find-exact-color-identities ;
192 : filter-temur-exact ( seq -- seq' ) { "G" "U" "R" } find-exact-color-identities ;
193
194 : filter-non-white ( seq -- seq' ) { "U" "B" "R" "G" } find-only-color-identities ;
195 : filter-non-blue ( seq -- seq' ) { "W" "B" "R" "G" } find-only-color-identities ;
196 : filter-non-black ( seq -- seq' ) { "W" "U" "R" "G" } find-only-color-identities ;
197 : filter-non-red ( seq -- seq' ) { "W" "U" "B" "G" } find-only-color-identities ;
198 : filter-non-green ( seq -- seq' ) { "W" "U" "B" "R" } find-only-color-identities ;
199
200 : filter-legalities ( seq name -- seq' ) '[ "legalities" of _ of "legal" = ] filter ;
201 : filter-standard ( seq -- seq' ) "standard" filter-legalities ;
202 : filter-future ( seq -- seq' ) "future" filter-legalities ;
203 : filter-historic ( seq -- seq' ) "historic" filter-legalities ;
204 : filter-timeless ( seq -- seq' ) "timeless" filter-legalities ;
205 : filter-gladiator ( seq -- seq' ) "gladiator" filter-legalities ;
206 : filter-pioneer ( seq -- seq' ) "pioneer" filter-legalities ;
207 : filter-explorer ( seq -- seq' ) "explorer" filter-legalities ;
208 : filter-modern ( seq -- seq' ) "modern" filter-legalities ;
209 : filter-legacy ( seq -- seq' ) "legacy" filter-legalities ;
210 : filter-pauper ( seq -- seq' ) "pauper" filter-legalities ;
211 : filter-vintage ( seq -- seq' ) "vintage" filter-legalities ;
212 : filter-penny ( seq -- seq' ) "penny" filter-legalities ;
213 : filter-commander ( seq -- seq' ) "commander" filter-legalities ;
214 : filter-oathbreaker ( seq -- seq' ) "oathbreaker" filter-legalities ;
215 : filter-standardbrawl ( seq -- seq' ) "standardbrawl" filter-legalities ;
216 : filter-brawl ( seq -- seq' ) "brawl" filter-legalities ;
217 : filter-alchemy ( seq -- seq' ) "alchemy" filter-legalities ;
218 : filter-paupercommander ( seq -- seq' ) "paupercommander" filter-legalities ;
219 : filter-duel ( seq -- seq' ) "duel" filter-legalities ;
220 : filter-oldschool ( seq -- seq' ) "oldschool" filter-legalities ;
221 : filter-premodern ( seq -- seq' ) "premodern" filter-legalities ;
222 : filter-predh ( seq -- seq' ) "predh" filter-legalities ;
223
224 : spanish-standard-cards ( -- seq )
225     scryfall-all-cards-json
226     filter-standard
227     [ "lang" of "es" = ] filter ;
228
229 : filter-red-any ( seq -- seq' ) [ "colors" of "R" swap member? ] filter ;
230 : filter-red-only ( seq -- seq' ) [ "colors" of { "R" } = ] filter ;
231 : filter-blue-any ( seq -- seq' ) [ "colors" of "U" swap member? ] filter ;
232 : filter-blue-only ( seq -- seq' ) [ "colors" of { "U" } = ] filter ;
233 : filter-green-any ( seq -- seq' ) [ "colors" of "G" swap member? ] filter ;
234 : filter-green-only ( seq -- seq' ) [ "colors" of { "G" } = ] filter ;
235 : filter-black-any ( seq -- seq' ) [ "colors" of "B" swap member? ] filter ;
236 : filter-black-only ( seq -- seq' ) [ "colors" of { "B" } = ] filter ;
237 : filter-white-any ( seq -- seq' ) [ "colors" of "W" swap member? ] filter ;
238 : filter-white-only ( seq -- seq' ) [ "colors" of { "W" } = ] filter ;
239 : filter-multi-color ( seq -- seq' ) [ "colors" of length 1 > ] filter ;
240 : filter-cmc= ( seq n -- seq' ) >float '[ "cmc" of _ = ] filter ;
241 : filter-cmc< ( seq n -- seq' ) >float '[ "cmc" of _ < ] filter ;
242 : filter-cmc<= ( seq n -- seq' ) >float '[ "cmc" of _ <= ] filter ;
243 : filter-cmc> ( seq n -- seq' ) >float '[ "cmc" of _ > ] filter ;
244 : filter-cmc>= ( seq n -- seq' ) >float '[ "cmc" of _ >= ] filter ;
245
246 : parse-type-line ( string -- pairs )
247     " // " split1
248     [
249         [
250             " â€” " split1
251             [ [ " " split ] ?call >array ] bi@ 2array
252         ] ?call
253     ] bi@ 2array sift ;
254
255 : type-line-of ( assoc -- string ) "type_line" of parse-type-line ;
256
257 : types-of ( assoc -- seq ) type-line-of [ first ] map concat ;
258 : subtypes-of ( assoc -- seq ) type-line-of [ second ] map concat ;
259
260 ! cards can have several type lines (one for each face)
261 : any-type? ( json name -- ? )
262     [ type-line-of ] dip >lower '[ first [ >lower ] map _ member-of? ] any? ;
263 : any-subtype? ( json name -- ? )
264     [ type-line-of ] dip >lower '[ second [ >lower ] map _ member-of? ] any? ;
265
266 : type-intersects? ( json types -- ? )
267     [ type-line-of ] dip [ >lower ] map '[ first [ >lower ] map _ intersects? ] any? ;
268 : subtype-intersects? ( json subtypes -- ? )
269     [ type-line-of ] dip [ >lower ] map '[ second [ >lower ] map _ intersects? ] any? ;
270
271 : filter-type ( seq text -- seq' ) '[ _ any-type? ] filter ;
272 : filter-subtype ( seq text -- seq' ) '[ _ any-subtype? ] filter ;
273 : filter-type-intersects ( seq text -- seq' ) '[ _ type-intersects? ] filter ;
274 : filter-subtype-intersects ( seq text -- seq' ) '[ _ subtype-intersects? ] filter ;
275
276 : filter-basic ( seq -- seq' ) [ "Basic" any-type? ] filter ;
277 : filter-basic-subtype ( seq text -- seq' ) [ filter-basic ] dip filter-subtype ;
278 : filter-land ( seq -- seq' ) [ "Land" any-type? ] filter ;
279 : filter-land-subtype ( seq text -- seq' ) [ filter-land ] dip filter-subtype ;
280 : filter-creature ( seq -- seq' ) [ "Creature" any-type? ] filter ;
281 : filter-creature-subtype ( seq text -- seq' ) [ filter-creature ] dip filter-subtype ;
282 : filter-emblem ( seq -- seq' ) [ "Emblem" any-type? ] filter ;
283 : filter-emblem-subtype ( seq text -- seq' ) [ filter-emblem ] dip filter-subtype ;
284 : filter-enchantment ( seq -- seq' ) [ "Enchantment" any-type? ] filter ;
285 : filter-enchantment-subtype ( seq text -- seq' ) [ filter-enchantment ] dip filter-subtype ;
286 : filter-saga ( seq -- seq' ) "saga" filter-enchantment-subtype ;
287 : filter-instant ( seq -- seq' ) [ "Instant" any-type? ] filter ;
288 : filter-instant-subtype ( seq text -- seq' ) [ filter-instant ] dip filter-subtype ;
289 : filter-sorcery ( seq -- seq' ) [ "Sorcery" any-type? ] filter ;
290 : filter-sorcery-subtype ( seq text -- seq' ) [ filter-sorcery ] dip filter-subtype ;
291 : filter-planeswalker ( seq -- seq' ) [ "Planeswalker" any-type? ] filter ;
292 : filter-planeswalker-subtype ( seq text -- seq' ) [ filter-planeswalker ] dip filter-subtype ;
293 : filter-legendary ( seq -- seq' ) [ "Legendary" any-type? ] filter ;
294 : filter-legendary-subtype ( seq text -- seq' ) [ filter-legendary ] dip filter-subtype ;
295 : filter-battle ( seq -- seq' ) [ "Battle" any-type? ] filter ;
296 : filter-battle-subtype ( seq text -- seq' ) [ filter-battle ] dip filter-subtype ;
297 : filter-artifact ( seq -- seq' ) [ "Artifact" any-type? ] filter ;
298 : filter-artifact-subtype ( seq text -- seq' ) [ filter-artifact ] dip filter-subtype ;
299
300 : reject-basic ( seq -- seq' ) [ "Basic" any-type? ] reject ;
301 : reject-land ( seq -- seq' ) [ "Land" any-type? ] reject ;
302 : reject-creature ( seq -- seq' ) [ "Creature" any-type? ] reject ;
303 : reject-emblem ( seq -- seq' ) [ "Emblem" any-type? ] reject ;
304 : reject-enchantment ( seq -- seq' ) [ "Enchantment" any-type? ] reject ;
305 : reject-instant ( seq -- seq' ) [ "Instant" any-type? ] reject ;
306 : reject-sorcery ( seq -- seq' ) [ "Sorcery" any-type? ] reject ;
307 : reject-planeswalker ( seq -- seq' ) [ "Planeswalker" any-type? ] reject ;
308 : reject-legendary ( seq -- seq' ) [ "Legendary" any-type? ] reject ;
309 : reject-battle ( seq -- seq' ) [ "Battle" any-type? ] reject ;
310 : reject-artifact ( seq -- seq' ) [ "Artifact" any-type? ] reject ;
311
312 : filter-mounts ( seq -- seq' ) "mount" filter-subtype ;
313 : filter-vehicles ( seq -- seq' ) "vehicle" filter-subtype ;
314 : filter-adventure ( seq -- seq' ) "adventure" filter-subtype ;
315 : filter-aura ( seq -- seq' ) "aura" filter-subtype ;
316 : filter-aura-subtype ( seq text -- seq' ) [ filter-aura ] dip filter-subtype ;
317 : filter-equipment ( seq -- seq' ) "Equipment" filter-subtype ;
318 : filter-equipment-subtype ( seq text -- seq' ) [ filter-equipment ] dip filter-subtype ;
319
320 : filter-common ( seq -- seq' ) '[ "rarity" of "common" = ] filter ;
321 : filter-uncommon ( seq -- seq' ) '[ "rarity" of "uncommon" = ] filter ;
322 : filter-rare ( seq -- seq' ) '[ "rarity" of "rare" = ] filter ;
323 : filter-mythic ( seq -- seq' ) '[ "rarity" of "mythic" = ] filter ;
324
325 : standard-cards ( -- seq' ) mtg-oracle-cards filter-standard ;
326 : historic-cards ( -- seq' ) mtg-oracle-cards filter-historic ;
327 : modern-cards ( -- seq' ) mtg-oracle-cards filter-modern ;
328
329 : sort-by-cmc ( assoc -- assoc' ) [ "cmc" of ] sort-by ;
330 : histogram-by-cmc ( assoc -- assoc' ) [ "cmc" of ] histogram-by sort-keys ;
331
332 : filter-by-itext-prop ( seq string prop -- seq' )
333     swap >lower '[ _ of >lower _ subseq-of? ] filter ;
334
335 : filter-by-text-prop ( seq string prop -- seq' )
336     swap '[ _ of _ subseq-of? ] filter ;
337
338 : map-card-faces ( json quot -- seq )
339     '[ [ "card_faces" of ] [ ] [ 1array ] ?if _ map ] map ; inline
340
341 : all-card-types ( seq -- seq' )
342     [ "type_line" of ] map-card-faces
343     concat members sort ;
344
345 : card>faces ( assoc -- seq )
346     [ "card_faces" of ] [ ] [ 1array ] ?if ;
347
348 : filter-card-faces-sub-card ( seq quot -- seq )
349     [ [ card>faces ] map concat ] dip filter ; inline
350
351 : filter-card-faces-sub-card-prop ( seq string prop -- seq' )
352     swap '[ _ of _ subseq-of? ] filter-card-faces-sub-card ;
353
354 : filter-card-faces-sub-card-iprop ( seq string prop -- seq' )
355     swap >lower '[ _ of >lower _ subseq-of? ] filter-card-faces-sub-card ;
356
357 : filter-card-faces-main-card ( seq quot -- seq )
358     dup '[ [ "card_faces" of ] [ _ any? ] _ ?if ] filter ; inline
359
360 : filter-card-faces-main-card-prop ( seq string prop -- seq' )
361     swap '[ _ of _ subseq-of? ] filter-card-faces-main-card ;
362
363 : filter-card-faces-main-card-iprop ( seq string prop -- seq' )
364     swap >lower '[ _ of >lower _ subseq-of? ] filter-card-faces-main-card ;
365
366 : filter-by-flavor-text ( seq string -- seq' )
367     "flavor_text" filter-card-faces-main-card-prop ;
368
369 : filter-by-flavor-itext ( seq string -- seq' )
370     "flavor_text" filter-card-faces-main-card-iprop ;
371
372 : filter-by-oracle-text ( seq string -- seq' )
373     "oracle_text" filter-card-faces-main-card-prop ;
374
375 : filter-by-oracle-itext ( seq string -- seq' )
376     "oracle_text" filter-card-faces-main-card-iprop ;
377
378 : filter-by-name-text ( seq string -- seq' ) "name" filter-by-text-prop ;
379 : filter-by-name-itext ( seq string -- seq' ) "name" filter-by-itext-prop ;
380
381 : filter-create-treasure ( seq -- seq' ) "create a treasure token" filter-by-oracle-itext ;
382 : filter-treasure-token ( seq -- seq' ) "treasure token" filter-by-oracle-itext ;
383 : filter-create-blood-token ( seq -- seq' ) "create a blood token" filter-by-oracle-itext ;
384 : filter-blood-token ( seq -- seq' ) "blood token" filter-by-oracle-itext ;
385 : filter-create-map-token ( seq -- seq' ) "create a map token" filter-by-oracle-itext ;
386 : filter-map-token ( seq -- seq' ) "map token" filter-by-oracle-itext ;
387
388 : filter-affinity ( seq -- seq' ) "affinity" filter-by-oracle-itext ;
389 : filter-backup ( seq -- seq' ) "backup" filter-by-oracle-itext ;
390 : filter-blitz ( seq -- seq' ) "blitz" filter-by-oracle-itext ;
391 : filter-compleated ( seq -- seq' ) "compleated" filter-by-oracle-itext ;
392 : filter-corrupted ( seq -- seq' ) "corrupted" filter-by-oracle-itext ;
393 : filter-counter ( seq -- seq' ) "counter" filter-by-oracle-itext ;
394 : filter-crew ( seq -- seq' ) "crew" filter-by-oracle-itext ;
395 : filter-cycling ( seq -- seq' ) "cycling" filter-by-oracle-itext ;
396 : filter-deathtouch ( seq -- seq' ) "deathtouch" filter-by-oracle-itext ;
397 : filter-defender ( seq -- seq' ) "defender" filter-by-oracle-itext ;
398 : filter-descend ( seq -- seq' ) "descend" filter-by-oracle-itext ;
399 : filter-destroy-target ( seq -- seq' ) "destroy target" filter-by-oracle-itext ;
400 : filter-discover ( seq -- seq' ) "discover" filter-by-oracle-itext ;
401 : filter-disguise ( seq -- seq' ) "disguise" filter-by-oracle-itext ;
402 : filter-domain ( seq -- seq' ) "domain" filter-by-oracle-itext ;
403 : filter-double-strike ( seq -- seq' ) "double strike" filter-by-oracle-itext ;
404 : filter-equip ( seq -- seq' ) "equip" filter-by-oracle-itext ;
405 : filter-equip-n ( seq -- seq' ) "equip {" filter-by-oracle-itext ;
406 : filter-exile ( seq -- seq' ) "exile" filter-by-oracle-itext ;
407 : filter-fights ( seq -- seq' ) "fights" filter-by-oracle-itext ;
408 : filter-first-strike ( seq -- seq' ) "first strike" filter-by-oracle-itext ;
409 : filter-flash ( seq -- seq' ) "flash" filter-by-oracle-itext ;
410 : filter-flying ( seq -- seq' ) "flying" filter-by-oracle-itext ;
411 : filter-for-mirrodin ( seq -- seq' ) "for mirrodin!" filter-by-oracle-itext ;
412 : filter-graveyard ( seq -- seq' ) "graveyard" filter-by-oracle-itext ;
413 : filter-haste ( seq -- seq' ) "haste" filter-by-oracle-itext ;
414 : filter-hideaway ( seq -- seq' ) "hideaway" filter-by-oracle-itext ;
415 : filter-hexproof ( seq -- seq' ) "hexproof" filter-by-oracle-itext ;
416 : filter-indestructible ( seq -- seq' ) "indestructible" filter-by-oracle-itext ;
417 : filter-investigate ( seq -- seq' ) "investigate" filter-by-oracle-itext ;
418 : filter-lifelink ( seq -- seq' ) "lifelink" filter-by-oracle-itext ;
419 : filter-madness ( seq -- seq' ) "madness" filter-by-oracle-itext ;
420 : filter-menace ( seq -- seq' ) "menace" filter-by-oracle-itext ;
421 : filter-mill ( seq -- seq' ) "mill" filter-by-oracle-itext ;
422 : filter-ninjutsu ( seq -- seq' ) "ninjutsu" filter-by-oracle-itext ;
423 : filter-proliferate ( seq -- seq' ) "proliferate" filter-by-oracle-itext ;
424 : filter-protection ( seq -- seq' ) "protection" filter-by-oracle-itext ;
425 : filter-prowess ( seq -- seq' ) "prowess" filter-by-oracle-itext ;
426 : filter-reach ( seq -- seq' ) "reach" filter-by-oracle-itext ;
427 : filter-read-ahead ( seq -- seq' ) "read ahead" filter-by-oracle-itext ;
428 : filter-reconfigure ( seq -- seq' ) "reconfigure" filter-by-oracle-itext ;
429 : filter-role ( seq -- seq' ) "role" filter-by-oracle-itext ;
430 : filter-sacrifice ( seq -- seq' ) "sacrifice" filter-by-oracle-itext ;
431 : filter-scry ( seq -- seq' ) "scry" filter-by-oracle-itext ;
432 : filter-shroud ( seq -- seq' ) "shroud" filter-by-oracle-itext ;
433 : filter-token ( seq -- seq' ) "token" filter-by-oracle-itext ;
434 : filter-toxic ( seq -- seq' ) "toxic" filter-by-oracle-itext ;
435 : filter-trample ( seq -- seq' ) "trample" filter-by-oracle-itext ;
436 : filter-vehicle ( seq -- seq' ) "vehicle" filter-by-oracle-itext ;
437 : filter-vigilance ( seq -- seq' ) "vigilance" filter-by-oracle-itext ;
438 : filter-ward ( seq -- seq' ) "ward" filter-by-oracle-itext ;
439
440 : filter-day ( seq -- seq' ) "day" filter-by-oracle-itext ;
441 : filter-night ( seq -- seq' ) "night" filter-by-oracle-itext ;
442 : filter-daybound ( seq -- seq' ) "daybound" filter-by-oracle-itext ;
443 : filter-nightbound ( seq -- seq' ) "nightbound" filter-by-oracle-itext ;
444
445 : filter-cave ( seq -- seq' ) "cave" filter-land-subtype ;
446 : filter-sphere ( seq -- seq' ) "sphere" filter-land-subtype ;
447
448 : filter-mount ( seq -- seq' ) "mount" filter-by-oracle-itext ;
449 : filter-outlaw ( seq -- seq' )
450     { "Assassin" "Mercenary" "Pirate" "Rogue" "Warlock" } filter-subtype-intersects ;
451 : filter-plot ( seq -- seq' ) "plot" filter-by-oracle-itext ;
452 : filter-saddle ( seq -- seq' ) "saddle" filter-by-oracle-itext ;
453 : filter-spree ( seq -- seq' ) "saddle" filter-by-oracle-itext ;
454
455 : power>n ( string -- n/f )
456     [ "*" = ] [ drop -1 ] [ string>number ] ?if ;
457
458 : mtg<  ( string/n/f n -- seq' ) [ power>n ] dip { [ and ] [ < ] } 2&& ;
459 : mtg<= ( string/n/f n -- seq' ) [ power>n ] dip { [ and ] [ <= ] } 2&& ;
460 : mtg>  ( string/n/f n -- seq' ) [ power>n ] dip { [ and ] [ > ] } 2&& ;
461 : mtg>= ( string/n/f n -- seq' ) [ power>n ] dip { [ and ] [ >= ] } 2&& ;
462 : mtg=  ( string/n/f n -- seq' ) [ power>n ] dip { [ and ] [ = ] } 2&& ;
463
464 : filter-power=* ( seq -- seq' ) [ "power" of "*" = ] filter-card-faces-main-card ;
465 : filter-toughness=* ( seq -- seq' ) [ "toughness" of "*" = ] filter-card-faces-main-card ;
466
467 : filter-power= ( seq n -- seq' ) '[ "power" of _ mtg= ] filter-card-faces-main-card ;
468 : filter-power< ( seq n -- seq' ) '[ "power" of _ mtg< ] filter-card-faces-main-card ;
469 : filter-power> ( seq n -- seq' ) '[ "power" of _ mtg> ] filter-card-faces-main-card ;
470 : filter-power<= ( seq n -- seq' ) '[ "power" of _ mtg<= ] filter-card-faces-main-card ;
471 : filter-power>= ( seq n -- seq' ) '[ "power" of _ mtg>= ] filter-card-faces-main-card ;
472
473 : filter-toughness= ( seq n -- seq' ) '[ "toughness" of _ mtg= ] filter-card-faces-main-card ;
474 : filter-toughness< ( seq n -- seq' ) '[ "toughness" of _ mtg< ] filter-card-faces-main-card ;
475 : filter-toughness> ( seq n -- seq' ) '[ "toughness" of _ mtg> ] filter-card-faces-main-card ;
476 : filter-toughness<= ( seq n -- seq' ) '[ "toughness" of _ mtg<= ] filter-card-faces-main-card ;
477 : filter-toughness>= ( seq n -- seq' ) '[ "toughness" of _ mtg>= ] filter-card-faces-main-card ;
478
479 : map-props ( seq props -- seq' ) '[ _ intersect-keys ] map ;
480
481 : gadgets. ( seq -- )
482     1 cut*
483     [ output-stream get '[ _ write-gadget ] each ]
484     [ output-stream get '[ _ print-gadget ] each ] bi* ;
485
486 : images. ( seq -- ) [ <image-gadget> ] map gadgets. ;
487
488 : normal-images-grid. ( seq -- )
489     4 group
490     [ [ card>image-uris ] map concat download-normal-images images. ] each ;
491
492 : small-card. ( assoc -- )
493     card>image-uris download-small-images images. ;
494
495 : small-cards. ( seq -- ) [ small-card. ] each ;
496
497 : normal-card. ( assoc -- )
498     card>image-uris download-normal-images images. ;
499
500 : normal-cards. ( seq -- ) [ normal-card. ] each ;
501 : standard-cards. ( seq -- ) filter-standard normal-cards. ;
502 : historic-cards. ( seq -- ) filter-historic normal-cards. ;
503 : modern-cards. ( seq -- ) filter-modern normal-cards. ;
504
505 ! rarity is only on main card `json` (if there are two faces)
506 : card-face-summary. ( json seq -- )
507     {
508         [ nip "name" of write bl ]
509         [ nip "mana_cost" of ?print ]
510         [ nip "type_line" of ?write ]
511         [ drop bl "--" write bl "rarity" of >title ?print ]
512         [ nip [ "power" of ] [ "toughness" of ] bi 2dup and [ "/" glue print ] [ 2drop ] if ]
513         [ nip "oracle_text" of ?print ]
514     } 2cleave nl ;
515
516 : card-face-summaries. ( json seq -- ) [ card-face-summary. ] with each ;
517
518 : card-summary. ( assoc -- )
519     dup
520     [ "card_faces" of ]
521     [ [ length number>string "Card Faces: " prepend print ] [ card-face-summaries. ] bi ]
522     [ card-face-summary. ] ?if nl nl nl ;
523
524 : card-summaries. ( seq -- ) [ card-summary. ] each ;
525
526 : card-summary-with-pic. ( assoc -- )
527     [ normal-card. ]
528     [ card-summary. ] bi ;
529
530 : card-summaries-with-pics. ( seq -- ) [ card-summary-with-pic. ] each ;
531
532 : standard-dragons. ( -- )
533     standard-cards
534     "Dragon" filter-creature-subtype
535     sort-by-cmc
536     normal-cards. ;
537
538 : collect-by-cmc ( seq -- seq' ) [ "cmc" of ] collect-by ;
539
540 MEMO: mtg-sets-by-abbrev ( -- assoc )
541     scryfall-all-cards-json
542     [ [ "set" of ] [ "set_name" of ] bi ] H{ } map>assoc ;
543
544 MEMO: mtg-sets-by-name ( -- assoc )
545     scryfall-all-cards-json
546     [ [ "set_name" of ] [ "set" of ] bi ] H{ } map>assoc ;
547
548 : filter-mtg-set ( seq abbrev -- seq ) '[ "set" of _ = ] filter ;
549
550 : unique-set-names ( seq -- seq' ) [ "set_name" of ] map members ;
551 : unique-set-abbrevs ( seq -- seq' ) [ "set" of ] map members ;
552
553 : standard-set-names ( -- seq ) standard-cards unique-set-names ;
554 : standard-set-abbrevs ( -- seq ) standard-cards unique-set-abbrevs ;
555
556
557 : sets-by-release-date ( -- assoc )
558     scryfall-all-cards-json
559     [ [ "set_name" of ] [ "released_at" of ] bi ] H{ } map>assoc
560     sort-values ;
561
562 : collect-cards-by-set-abbrev ( seq -- assoc ) [ "set" of ] collect-by ;
563 : collect-cards-by-set-name ( seq -- assoc ) [ "set_name" of ] collect-by ;
564 : cards-by-set-abbrev ( -- assoc ) mtg-oracle-cards collect-cards-by-set-abbrev ;
565 : cards-by-set-name ( -- assoc ) mtg-oracle-cards collect-cards-by-set-name ;
566
567 : filter-set ( seq abbrev -- seq ) >lower '[ "set" of _ = ] filter ;
568 : filter-set-intersect ( seq abbrevs -- seq ) [ >lower ] map '[ "set" of _ member? ] filter ;
569
570 : mid-cards ( -- seq ) mtg-oracle-cards "mid" filter-set ;
571 : vow-cards ( -- seq ) mtg-oracle-cards "vow" filter-set ;
572 : neo-cards ( -- seq ) mtg-oracle-cards "neo" filter-set ;
573 : snc-cards ( -- seq ) mtg-oracle-cards "snc" filter-set ;
574 : dmu-cards ( -- seq ) mtg-oracle-cards "dmu" filter-set ;
575 : bro-cards ( -- seq ) mtg-oracle-cards "bro" filter-set ;
576 : one-cards ( -- seq ) mtg-oracle-cards "one" filter-set ;
577 : mom-cards ( -- seq ) mtg-oracle-cards "mom" filter-set ;
578 : mat-cards ( -- seq ) mtg-oracle-cards "mat" filter-set ;
579 : woe-cards ( -- seq ) mtg-oracle-cards "woe" filter-set ;
580 : woe-cards-bonus ( -- seq ) mtg-oracle-cards [ "set" of "wot" = ] filter-set ;
581 : woe-cards-all ( -- seq ) mtg-oracle-cards { "woe" "wot" } filter-set-intersect ;
582 : lci-cards ( -- seq ) mtg-oracle-cards "lci" filter-set ;
583 : mkm-cards ( -- seq ) mtg-oracle-cards "mkm" filter-set ;
584 : otj-cards ( -- seq ) mtg-oracle-cards "otj" filter-set ;
585 : otj-cards-bonus ( -- seq ) mtg-oracle-cards "big" filter-set ;
586 : otj-cards-all ( -- seq ) mtg-oracle-cards { "otj" "big" } filter-set-intersect ;
587
588 : sort-by-colors ( seq -- seq' )
589     {
590         { [ "color_identity" of length ] <=> }
591         { [ "color_identity" of sort ?first "A" or ] <=> }
592         { [ "cmc" of ] <=> }
593         { [ "mana_cost" of length ] <=> }
594         { [ "creature" any-type? -1 1 ? ] <=> }
595         { [ "power" of -1 1 ? ] <=> }
596         { [ "toughness" of -1 1 ? ] <=> }
597         { [ "name" of ] <=> }
598     } sort-with-spec ;
599
600 : cards-by-color. ( seq -- ) sort-by-colors normal-cards. ;
601
602 CONSTANT: rarity-to-number H{
603     { "common" 0 }
604     { "uncommon" 1 }
605     { "rare" 2 }
606     { "mythic" 3 }
607 }
608
609 : sort-by-rarity ( seq -- seq' )
610     {
611         { [ "rarity" of rarity-to-number at ] <=> }
612         { [ "color_identity" of length ] <=> }
613         { [ "color_identity" of sort ?first "A" or ] <=> }
614         { [ "cmc" of ] <=> }
615         { [ "mana_cost" of length ] <=> }
616         { [ "name" of ] <=> }
617     } sort-with-spec ;
618
619 : cards-by-rarity. ( seq -- ) sort-by-rarity normal-cards. ;
620
621 : sort-by-release ( seq -- seq' )
622     {
623         { [ "released_at" of ymd>timestamp ] <=> }
624         { [ "set" of ] <=> }
625     } sort-with-spec ;
626
627 : cards-by-release. ( seq -- ) sort-by-release normal-cards. ;
628
629 : sort-by-set-colors ( seq -- seq' )
630     {
631         { [ "released_at" of ymd>timestamp ] <=> }
632         { [ "set" of ] <=> }
633         { [ "color_identity" of length ] <=> }
634         { [ "color_identity" of sort ?first "A" or ] <=> }
635         { [ "cmc" of ] <=> }
636         { [ "mana_cost" of length ] <=> }
637         { [ "creature" any-type? -1 1 ? ] <=> }
638         { [ "power" of -1 1 ? ] <=> }
639         { [ "toughness" of -1 1 ? ] <=> }
640         { [ "name" of ] <=> }
641     } sort-with-spec ;
642
643 : cards-by-set-colors. ( seq -- ) sort-by-set-colors normal-cards. ;
644
645 : cards-by-name ( name -- seq' ) [ mtg-oracle-cards ] dip filter-by-name-itext sort-by-release ;
646 : card-by-name ( name -- card )
647     [ mtg-oracle-cards ] dip >lower
648     [ '[ "name" of >lower _ = ] filter ?first ]
649     [ '[ "name" of >lower _ head? ] filter ?first ] 2bi or ;
650 : cards-by-name. ( name -- ) cards-by-name normal-cards. ;
651 : standard-cards-by-name. ( name -- ) cards-by-name standard-cards. ;
652 : historic-cards-by-name. ( name -- ) cards-by-name historic-cards. ;
653 : modern-cards-by-name. ( name -- ) cards-by-name modern-cards. ;
654
655 : paren-set? ( string -- ? )
656     { [ "(" head? ] [ ")" tail? ] [ length 5 = ] } 1&& ;
657
658 : remove-set-and-num ( string -- string' )
659     " " split
660     dup 2 ?lastn
661     [ paren-set? ] [ string>number ] bi* and [
662         2 head*
663     ] when " " join ;
664
665 : assoc>cards ( assoc -- seq )
666     [ card-by-name <array> ] { } assoc>map concat ;
667
668 : parse-mtga-card-line ( string -- array )
669     [ blank? ] trim
670     " " split1
671     [ string>number ]
672     [ remove-set-and-num card-by-name ] bi* <array> ;
673
674 : parse-mtga-cards ( strings -- seq )
675     [ parse-mtga-card-line ] map concat ;
676
677 TUPLE: mtga-deck name deck sideboard section ;
678
679 : <mtga-deck> ( -- mtga-deck )
680     mtga-deck new "Deck" >>section ;
681
682 : <moxfield-deck> ( name deck sideboard -- deck )
683     mtga-deck new
684         swap >>sideboard
685         swap >>deck
686         swap >>name ;
687
688 ERROR: unknown-mtga-deck-section section ;
689 : parse-mtga-deck ( string -- mtga-deck )
690     string-lines [ [ blank? ] trim ] map harvest
691     { "About" "Deck" "Sideboard" } split*
692     [ <mtga-deck> ] dip
693     [
694         dup { "About" "Deck" "Sideboard" } intersects? [
695             first >>section
696         ] [
697             over section>> {
698                 { "About" [ first "Name " ?head drop [ blank? ] trim >>name ] }
699                 { "Deck" [ parse-mtga-cards >>deck ] }
700                 { "Sideboard" [ parse-mtga-cards >>sideboard ] }
701                 [
702                     unknown-mtga-deck-section
703                 ]
704             } case
705         ] if
706     ] each ;
707
708 : sort-by-deck-order ( seq -- seq' )
709     [ "Land" any-type? not ] partition
710     [ sort-by-set-colors ] bi@ append ;
711
712 : cards. ( seq -- ) sort-by-deck-order normal-cards. ;
713
714 : sideboard. ( seq -- )
715     sideboard>> [ "Sideboard" print sort-by-deck-order normal-cards. ] when* ;
716
717 GENERIC: deck. ( obj -- )
718
719 M: string deck. parse-mtga-deck deck. ;
720
721 M: mtga-deck deck. [ name>> ?print ] [ deck>> cards. ] bi ;
722
723 M: sequence deck. cards. ;
724
725 GENERIC: deck-and-sideboard. ( mtga-deck -- )
726
727 M: string deck-and-sideboard. parse-mtga-deck deck-and-sideboard. ;
728
729 M: mtga-deck deck-and-sideboard. [ deck. ] [ sideboard. ] bi ;
730
731 M: sequence deck-and-sideboard. deck. ;
732
733 : filter-mtg-cheat-sheet ( seq -- seq' )
734     [
735         {
736             [ filter-instant ]
737             [ filter-flash ]
738             [ filter-cycling ]
739             [ filter-disguise ]
740             [ filter-madness ]
741         } cleave
742     ] { } append-outputs-as sort-by-colors ;
743
744 : mtg-cheat-sheet. ( seq -- ) filter-mtg-cheat-sheet normal-cards. ;
745 : mtg-cheat-sheet-text. ( seq -- ) filter-mtg-cheat-sheet card-summaries. ;
746
747 MEMO: get-moxfield-user ( username -- json )
748     "https://api2.moxfield.com/v2/users/%s/decks?pageNumber=1&pageSize=100" sprintf http-get-json nip ;
749
750 MEMO: get-moxfield-deck ( public-id -- json )
751     "https://api2.moxfield.com/v3/decks/all/" prepend http-get-json nip ;
752
753 : moxfield-board>cards ( board -- seq )
754     "cards" of values [
755         [ "quantity" of ] [ "card" of "name" of ] bi 2array
756     ] map assoc>cards ;
757
758 : json>moxfield-deck ( json -- mtga-deck )
759     [ "name" of ]
760     [
761         "boards" of
762         [ "mainboard" of moxfield-board>cards ]
763         [ "sideboard" of moxfield-board>cards ] bi
764     ] bi
765     <moxfield-deck> ;
766
767 : moxfield-random-deck-for-username ( username -- json )
768     get-moxfield-user
769     "data" of
770     random "publicId" of get-moxfield-deck
771     json>moxfield-deck ;
772
773 : moxfield-latest-deck-for-username ( username -- json )
774     get-moxfield-user
775     "data" of ?first "publicId" of get-moxfield-deck
776     json>moxfield-deck ;
777
778 : moxfield-latest-deck-for-username. ( username -- )
779     moxfield-latest-deck-for-username deck. ;
780
781 : moxfield-latest-deck-and-sideboard-for-username. ( username -- )
782     moxfield-latest-deck-for-username deck-and-sideboard. ;