Skip to content

Conformance fixtures ​

Test files for parser authors. A conforming parser accepts every file in valid/ and rejects every file in invalid/. The spec (spec/mtgr.md) is authoritative: if a fixture disagrees with it, the fixture is a bug.

Each invalid file is a valid file with exactly one rule broken, so a parser that rejects it for a different reason has found the wrong problem. Most are made from valid/basic.mtgr; unescaped-quote is made from valid/card-names.mtgr. Diff an invalid file against its base to see the change.

Some checks need the game replayed, not just parsed (spec section 14): stack snapshots (check 6), keyframes (check 7), and what resolves (check 8). A parser that does not replay games cannot detect stack-mismatch, keyframe-mismatch, or resolve-not-on-top, and does not conform fully.

All fixtures are format 0.3.0.

Valid ​

FileWhat it exercises
basic.mtgrA short complete game: opening hands, a mana ability, a spell with a target, priority and stack snapshots, a reveal, a discard, keyframes, and a concession.
minimal.mtgrThe smallest useful game: unknown tag values, an unknown decklist, no turns, and a concession before the opening hands.
card-names.mtgrCard names that collide with the syntax: an escaped quote (Kongming, \"Sleeping Dragon\"), the word by (Bound by Moonsilver, which is also the discarded card), a trailing ? (Continue?), a colon (Avengers: Under Siege), and non-ASCII letters including U+A789 (Ratonhnhaké꞉ton).
two-games.mtgrTwo games of one match in one file, separated by one empty line. Game 2 has no roll: the loser of game 1 chooses (CR 103.1). Also an escape line, an extension tag, and an extension statement, all of which parsers must ignore.
annotations.mtgrAnnotations and variations: several NAGs on one line, a brace comment containing quotes, by, and ;, a line comment containing braces, an empty line comment, and a nested variation with a hypothetical ID.
late-start.mtgrA recording that starts late and loses track: incomplete before the first turn, missing turns 1 to 3, objects introduced before the keyframe, times with fractions, ability text ? (the source did not report Dark Confidant's text), a by ? cause (the source did not say why life was lost), a second incomplete with no keyframe after it, and result *.
pregame-reveals.mtgrArchetype tags for both players; the opponent revealing a companion before the opening hands, listed in the keyframe with companion; Chancellor of the Annex revealed from an opening hand, creating an effect that the keyframe lists.
costs.mtgrCosts: Dismember with both Phyrexian symbols announced as life (choose symbol), cost lines for Thalia and then Trinisphere, life paid by cost:, a stack snapshot repeating the symbol choices, a creature dying to a state-based action after a pt change, and Murktide Regent cast with delve: exiled cards with it, entering with counters.

Invalid ​

FileSpec sectionRule broken
bad-escape.mtgr2.6A string contains a backslash followed by a space, which is not an escape.
cause-not-allowed.mtgr8, 14.1 (check 5)pass takes no cause.
choice-with-cause.mtgr7.2A choice line made by the controller has a cause.
crlf.mtgr2.2Lines end with CR LF.
double-space.mtgr2.5Two spaces between tokens.
empty-line-in-body.mtgr3An empty line inside a game body.
escape-line-in-tags.mtgr3An escape line inside the tag section.
generic-mana.mtgr9.4{1} is cost notation, not a type of mana.
hidden-with-name.mtgr6.3, 14.1 (check 4)A hidden object is written with a name it was never identified with.
id-before-new.mtgr14.1 (check 3)An ID is used before its new line.
id-gap.mtgr14.1 (check 3)IDs skip from #15 to #17.
identify-named.mtgr14.1 (check 15)identify is used on a card whose name was already known.
keyframe-mismatch.mtgr14.1 (check 7), 14.5The turn 2 keyframe gives PlayerA 20 life; the events leave them at 18.
leading-zero.mtgr2.8An integer with a leading zero.
line-after-end.mtgr14.1 (check 16)A game statement follows the concession that ended the game.
missing-cause.mtgr8, 14.1 (check 5)A move line has no cause.
missing-empty-line.mtgr3No empty line between the tag section and the body.
missing-stack.mtgr9.9, 14.1 (check 14)A priority line while the stack is not empty has no stack line.
name-mismatch.mtgr14.1 (check 4)#1 is a Swamp but is written with the name Island.
no-final-newline.mtgr2.2The file does not end with LF.
not-nfc.mtgr2.1A comment contains e followed by a combining acute accent (U+0301): the file is not in Normalization Form C.
odd-indent.mtgr5.2A step line is indented three spaces, which is no nesting level.
old-value-mismatch.mtgr14.1 (check 10)A life line's old value (19) is not PlayerA's life total (20).
pay-without-mana.mtgr14.1 (check 11)PlayerA pays {B} with an empty mana pool: the mana ability adds nothing.
perspective-unknown.mtgr4.2Perspective names neither Player1 nor Player2.
resolve-not-on-top.mtgr14.1 (check 8)Island resolves, but it is in PlayerA's hand, not on top of the stack.
result-contradicts-end.mtgr14.1 (check 16)PlayerB concedes, but the result says PlayerB won.
result-mismatch.mtgr14.1 (check 2)The result line (0-1) does not equal the Result tag (1-0).
skipped-step.mtgr9.2The upkeep step occurred but is not written.
stack-mismatch.mtgr14.1 (check 6), 14.6A stack snapshot gives Thoughtseize's target as PlayerA; it targets PlayerB.
tab-indent.mtgr2.4A tab is used for indentation.
tag-duplicate.mtgr4.1A tag name appears twice.
tag-optional-order.mtgr4.5Optional tags are not in ASCII order (UTCDate before Termination).
tag-required-order.mtgr4.2The required tags are not in the required order (Date before Site).
tap-tapped.mtgr14.1 (check 12)A tapped permanent is tapped again.
time-decreasing.mtgr5.1Time decreases from one main-line body line to the next.
trailing-text.mtgr16Text after a cause that is not a replacement, NAG, or comment.
turn-out-of-order.mtgr5.3Turn 2 is written as turn 3.
unbalanced-variation.mtgr12, 14.1 (check 17)A variation is opened with ( and never closed.
unclosed-brace-comment.mtgr11A brace comment is not closed on its line.
unescaped-quote.mtgr2.6A double quote inside a card name is not escaped.
unknown-keyword.mtgr2.9scry is not a defined keyword and has no extension prefix.
wrong-source-zone.mtgr14.1 (check 9)Swamp is played from the graveyard, but it is in PlayerA's hand.

Adding a fixture ​

Every change to a definition updates the fixtures it affects (CONTRIBUTING.md). A new invalid fixture breaks exactly one rule and gets a row in the table above.