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