Skip to content

Instantly share code, notes, and snippets.

@georgestephanis
Last active April 30, 2020 18:19
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save georgestephanis/f09859213064246ee3fc647fa0238876 to your computer and use it in GitHub Desktop.
Save georgestephanis/f09859213064246ee3fc647fa0238876 to your computer and use it in GitHub Desktop.
A validator script to confirm the validity and structure of Magic: The Gathering Arena Export/Import data. GPLv3

Arena Export Validator

This is designed as a validator designed around current Arena standards -- to be used to validate and confirm whether or not a platform's generated export format meets Arena's specs (as Arena has no officially published specs, I've tried to determine varied test cases including decks with both commanders and companions).

Assumed Format of Export Data

  • There are four possible sections, that appear in the following sequence: Commander, Companion, Deck, Sideboard.
  • The section title is the first line of each section.
  • Blank lines are inserted after each section.
  • If a line begins with an integer, it's a card. If it begins with anything else, it's a Section title.
  • The Commander section, if included, contains a single card, and that card resides ONLY in the Commander section.
  • The Companion section, if included, contains a single card, and that card is DUPLICATED and also is included in the Sideboard section.
  • The Deck is required. You can't have a deck without a deck.
  • The Sideboard section is optional, UNLESS your deck is using a Companion. If using a Companion, at least one copy of the Companion must also be included in the Sideboard.
  • Card lines are formatted as follows: [qty] [name] ([set]) [collectors number] and can be matched via the following regex: /^\d+ .+ \([\dA-Z]{3}\) \d+$/i
#!/usr/bin/env node
// by @georgestephanis, license: GPLv3
// usage: `pbpaste | ./arena-format-validator.js` or `cat commander-companion.arena.txt | node arena-format-validator.js`
function validateDeck( deck ) {
// Swap this to true if you want information about successful bits too.
const printInfo = false;
const deckArray = deck.split('\n');
const found = {};
let type, matches;
deckArray.forEach( function( line ) {
if ( line ) {
if ( line.match( /^\d/ ) ) {
matches = line.match( /^(?<quantity>\d+) (?<name>.+) \((?<set>[\da-zA-Z]{3})\) (?<setNumber>\d+)$/ );
if ( ! type ) {
if ( matches ) {
console.error( "\x1b[31m" + 'Error! Found a valid card without a type delimiter -- ' +
matches.groups.quantity + ' ' + matches.groups.name +
' (' + matches.groups.set + ') ' + matches.groups.setNumber + "\x1b[0m" );
} else {
console.error( "\x1b[31m" + 'Error! Found a malformed card without a type delimiter -- ' + line + "\x1b[0m" );
}
} else {
if ( matches ) {
printInfo && console.info( 'Found a ' + type + ' card -- ' +
matches.groups.quantity + ' ' + matches.groups.name +
' (' + matches.groups.set + ') ' + matches.groups.setNumber );
found[ type ].push( matches.groups );
} else {
console.error( "\x1b[31m" + 'Error! This ' + type + ' card is malformed -- ' + line + "\x1b[0m" );
}
}
} else {
printInfo && console.info( 'Found a section delimiter -- ' + line );
type = line;
if ( Array.isArray( found[ type ] ) ) {
console.error( "\x1b[31m" + 'Error! Section delimiter ' + type + ' found multiple times!' + "\x1b[0m" );
} else {
found[ type ] = [];
}
if ( ! [ 'Commander', 'Companion', 'Deck', 'Sideboard' ].includes( type ) ) {
console.error( "\x1b[31m" + 'Error! Section delimiter ' + type + ' is unfamiliar!' + "\x1b[0m" );
}
}
} else {
printInfo && console.info( 'Found a blank line.' + ( type ? ' Ending ' + type + ' section.' : '' ) );
type = null;
}
} );
if ( ! found.Deck ) {
console.error( "\x1b[31m" + 'Error! No "Deck" section was found!' + "\x1b[0m" );
}
if ( found ) {
console.log( "\n" );
console.log( 'Full parsed result:' );
console.log( "\n" );
for ( type in found ) {
console.log( type );
console.table( found[ type ] );
}
} else {
console.error( "\x1b[31m" + 'Error! No parseable data was found!' + "\x1b[0m" );
}
return found;
}
let deck = '';
process.stdin.resume();
process.stdin.setEncoding('utf8');
process.stdin.on( 'data', chunk => deck += chunk );
process.stdin.on( 'end', () => {
validateDeck( deck );
});
Commander
1 Daxos, Blessed by the Sun (THB) 9
Companion
1 Lurrus of the Dream Den (IKO) 226
Deck
1 Stonecoil Serpent (ELD) 235
1 Healer's Hawk (GRN) 14
22 Plains (IKO) 262
1 Ajani's Pridemate (WAR) 4
1 Soulmender (M20) 37
1 Hunted Witness (GRN) 15
1 Charmed Stray (WAR) 8
1 Beloved Princess (ELD) 7
1 Alseid of Life's Bounty (THB) 1
1 Charming Prince (ELD) 8
1 Daybreak Chaplain (M20) 12
1 Gingerbrute (ELD) 219
1 Impassioned Orator (RNA) 12
1 Hushbringer (ELD) 18
1 Grateful Apparition (WAR) 17
1 Drannith Healer (IKO) 10
1 Haazda Marshal (GRN) 13
1 Sworn Companions (GRN) 27
1 Shadowspear (THB) 236
1 The Birth of Meletis (THB) 5
1 Sentinel's Mark (RNA) 20
1 Battlefield Promotion (WAR) 5
1 Dawn of Hope (GRN) 8
1 Desperate Lunge (WAR) 266
1 Moment of Heroism (M20) 30
1 Rally for the Throne (ELD) 25
1 Triumphant Surge (THB) 41
1 Take Heart (GRN) 28
1 Light of Hope (IKO) 20
1 Golden Egg (ELD) 220
1 Shatter the Sky (THB) 37
1 Citywide Bust (GRN) 4
1 Unbreakable Formation (RNA) 29
1 Ravnica at War (WAR) 28
1 Inspired Charge (M20) 24
1 Faerie Guidemother (ELD) 11
1 Solid Footing (IKO) 31
1 Sentinel's Eyes (THB) 36
Sideboard
1 Lurrus of the Dream Den (IKO) 226
Companion
1 Gyruda, Doom of Depths (IKO) 221
Deck
5 Forest (THB) 287
1 Incubation Druid (RNA) 131
1 Plains (THB) 279
4 Fabled Passage (ELD) 244
1 Temple of Plenty (THB) 248
2 Temple of Mystery (M20) 255
1 Temple of Enlightenment (THB) 246
4 Temple Garden (GRN) 258
2 Island (THB) 281
4 Hallowed Fountain (RNA) 251
4 Breeding Pool (RNA) 246
4 Growth Spiral (RNA) 178
2 End-Raze Forerunners (RNA) 124
3 Gyruda, Doom of Depths (IKO) 221
1 Dream Trawler (THB) 214
2 Umori, the Collector (IKO) 231
4 Thassa, Deep-Dwelling (THB) 71
4 Spark Double (WAR) 68
3 Elite Guardmage (WAR) 195
4 Paradise Druid (WAR) 171
4 Charming Prince (ELD) 8
Sideboard
2 Disdainful Stroke (GRN) 37
2 Return to Nature (THB) 197
4 Aether Gust (M20) 42
1 Gyruda, Doom of Depths (IKO) 221
4 Destiny Spinner (THB) 168
2 Dovin's Veto (WAR) 193
Commander
1 Yarok, the Desecrated (M20) 220
Deck
1 Agent of Treachery (M20) 43
1 Arcane Signet (ELD) 331
1 Assassin's Trophy (GRN) 152
1 Bake into a Pie (ELD) 76
1 Breeding Pool (RNA) 246
1 Castle Locthwain (ELD) 241
1 Castle Vantress (ELD) 242
1 Cauldron Familiar (ELD) 81
1 Cavalier of Night (M20) 94
1 Cavalier of Thorns (M20) 167
1 Chromatic Lantern (GRN) 233
1 Command the Dreadhorde (WAR) 82
1 Dimir Guildgate (GRN) 246
1 Dismal Backwater (M20) 245
1 District Guide (GRN) 128
1 Fabled Passage (ELD) 244
1 Fblthp, the Lost (WAR) 50
1 Feasting Troll King (ELD) 152
1 Field of the Dead (M20) 247
1 Gilded Goose (ELD) 160
1 Gingerbread Cabin (ELD) 245
1 Gadwick, the Wizened (ELD) 48
1 Golden Egg (ELD) 220
1 Golgari Findbroker (GRN) 175
1 Golgari Guildgate (GRN) 249
1 Gravedigger (M19) 98
1 Hydroid Krasis (RNA) 183
1 Jungle Hollow (M20) 248
1 Thassa, Deep-Dwelling (THB) 71
1 Meteor Golem (M19) 241
1 Murderous Rider (ELD) 97
1 Overgrown Tomb (GRN) 253
1 Paradise Druid (WAR) 171
1 Plaza of Harmony (RNA) 254
1 Risen Reef (M20) 217
1 Savvy Hunter (ELD) 200
1 Simic Guildgate (RNA) 257
1 Spark Double (WAR) 68
1 Tamiyo, Collector of Tales (WAR) 220
1 Temple of Malady (M20) 254
1 Temple of Mystery (M20) 255
1 Tempting Witch (ELD) 108
1 The Great Henge (ELD) 161
1 Thornwood Falls (M20) 258
1 Tomebound Lich (M20) 219
1 Trail of Crumbs (ELD) 179
1 Voracious Hydra (M20) 200
1 Vraska, Golgari Queen (GRN) 213
1 Watery Grave (GRN) 259
1 Wicked Wolf (ELD) 181
1 Witch's Cottage (ELD) 249
1 Witch's Oven (ELD) 237
1 Yarok's Fenlurker (M20) 123
3 Forest (ELD) 266
1 Island (ELD) 254
2 Swamp (ELD) 258
@georgestephanis
Copy link
Author

The main purpose for this gist is to provide a consistent test that multiple providers and sites can all build to, to ensure their output validates.

To that end, it's built in Javascript, so that it can more easily be used on any platform, regardless of the server-side language.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment