Embed the playtester

The SlingLab playtester deals a decklist into a solo game on a full SlingLab board: draw, mulligan, play lands, and goldfish your curve. You can put it on your own page with an iframe, or link to it. The deck goes in the URL, so there's no API key, no account, and nothing to set up on our side.

<iframe
  src="https://slinglab.gg/playtest?list=1%20Sol%20Ring%0A1%20Arcane%20Signet%0A98%20Island"
  title="Playtest this deck on SlingLab"
  width="100%"
  height="720"
  style="border: 0"
  allow="fullscreen"
></iframe>

How it works

  • The game runs entirely in the visitor's browser. Nothing is saved, and refreshing deals a fresh game from the same URL.
  • Your page only supplies the decklist. SlingLab matches each line to a card and loads the card art.
  • Visitors don't need a SlingLab account. Inside an iframe they are always signed out, even if they're signed in on slinglab.gg. Links to sign in or sign up open in a new tab.
  • Only /playtest can be framed. Every other SlingLab page refuses to load in an iframe.

URL parameters

Parameter Required Value
list one of list or deck A plain decklist, URL-encoded. See Decklist format.
deck one of list or deck A compressed decklist. See Compressed decks. Use this for full 100-card lists.
format no The game format. Defaults to commander.

Either parameter accepts either form; SlingLab detects which one it got. The two names exist so your URLs read clearly.

Supported format values: commander, pedh, standard, modern, pauper, twoHeadedGiant, twoHeadedGiantCommander, and custom.

The playtester currently supports Magic: The Gathering decks only.

Decklist format

One card per line: a quantity, then the card name. This is the same text most deckbuilders export.

1 Norin the Wary [Commander]
1 Sol Ring
1x Lightning Bolt
1 Lightning Greaves (2xm) 236
30 Mountain

Sideboard
1 Pyroblast
  • The quantity may have an x after it (1x Lightning Bolt).
  • A set code and collector number in parentheses after the name pick a printing: (2xm) 236. Without one, SlingLab uses its default art for the card.
  • Mark a commander with [Commander] at the end of its line. Mark each partner or background the same way.
  • A line reading Sideboard or Maybeboard puts the lines below it in that board. A line reading Deck, Mainboard, or Commander switches back to the main deck. A Commander header line does not mark the cards under it as commanders; use [Commander] for that.
  • Blank lines and lines that don't start with a number are skipped.
  • Foil markers like *F* are ignored.

If none of the lines match a card, the playtester shows an error instead of a game. If only some match, the game starts with the cards that did.

Compressed decks

A full Commander list is several kilobytes once URL-encoded, which is long for a link and longer than some browsers, chat apps, and servers accept. The deck parameter takes the list as gzip-compressed JSON, base64url-encoded:

{
  "decklist": "1 Norin the Wary\n1 Sol Ring\n30 Mountain",
  "commander": "Norin the Wary"
}
  • decklist (required): the list, in the decklist format above.
  • commander (optional): the commander's exact card name.
  • commanders (optional): an array of names, for partners and backgrounds. You can send commander, commanders, or both.

Encode it as base64url with no padding: - and _ instead of + and /, and no trailing =. A base64url string is safe in a URL as-is, with no further encoding.

JavaScript (browser)

async function playtestUrl(decklist, commander) {
  const json = JSON.stringify({ decklist, commander });
  const gzipped = new Response(
    new Blob([json]).stream().pipeThrough(new CompressionStream("gzip")),
  );
  const bytes = new Uint8Array(await gzipped.arrayBuffer());
  let binary = "";
  for (const b of bytes) binary += String.fromCharCode(b);
  const deck = btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
  return `https://slinglab.gg/playtest?deck=${deck}`;
}

Node.js

import { gzipSync } from "node:zlib";

function playtestUrl(decklist, commander) {
  const json = JSON.stringify({ decklist, commander });
  const deck = gzipSync(json).toString("base64url");
  return `https://slinglab.gg/playtest?deck=${deck}`;
}

Python

import base64, gzip, json

def playtest_url(decklist, commander=None):
    payload = json.dumps({"decklist": decklist, "commander": commander}).encode()
    deck = base64.urlsafe_b64encode(gzip.compress(payload)).rstrip(b"=").decode()
    return f"https://slinglab.gg/playtest?deck={deck}"

Limits

Limit Value
Decklist length, after decoding 20,000 characters
Decklist lines, after decoding 500
Compressed deck value 100,000 characters
Decompressed deck payload 200 KB
Decks loaded per visitor 20 per minute, per IP address

A list over these limits shows an error instead of a game. The per-visitor limit applies to each visitor's own browser, not to your site as a whole.

Sizing the frame

The board fills whatever space it gets and brings its own controls. It needs room to be playable:

  • Give it the full width of your content column and at least 600px of height. 720px or more is better.
  • On phones, a full-screen link usually works better than an inline frame. Use the same URL as a normal link (<a href="https://slinglab.gg/playtest?deck=...">), and the playtester opens as a full page.
  • allow="fullscreen" lets visitors expand the board to the whole screen.

Linking instead of embedding

Every embed URL also works as a plain link. If you'd rather not host a frame, send players to the same URL. They get the playtester as a full page on slinglab.gg, signed in if they already have an account.