Library for VTES cards and TWDA.
Project description
KRCG
A Python package built to serve as an interface for the VEKN official card texts, the Tournament Winning Deck Archive (TWDA), and the cards rulings database.
Portions of the materials are the copyrights and trademarks of Paradox Interactive AB, and are used with permission. All rights reserved. For more information please visit white-wolf.com.
Upgrading from 4.x? 5.0 is a ground-up rewrite with a new API. See Migrating from 4.x.
Offspring projects
The KRCG library has been used in multiple offspring projects:
-
krcg-cli is a convenient Command Line Interface over the library
-
krcg-static is used to generate easy-to-use static files for web developers. It is available online at static.krcg.org
-
krcg-api is a free RESTful web API to get the most out of the library for web projects. It is available online at v2.api.krcg.org
-
krcg-bot is a friendly Discord bot that provides official card text and rulings for free. It is available for free.
Installation
Python 3 (>=3.12) is required.
Use pip to install the krcg tool:
pip install krcg
Using the library
KRCG is a Python library for VTES.
The code is well-documented and can be explored with Python's built-in help.
Loading the cards
krcg.load() returns a CardDict — the cards library. There are three loaders:
| Function | Source | Mode |
|---|---|---|
krcg.load() |
version-keyed cache, else the packaged data | sync |
krcg.load_local() |
always the packaged data (fast and offline) | sync |
krcg.load_online(session) |
the up-to-date JSON on static.krcg.org | async |
>>> import krcg
>>> cards = krcg.load()
>>> cards["Alastor"].full_name
'Alastor'
>>> cards["Alastor"].url
'https://static.krcg.org/card/alastor.jpg'
>>> cards[100038] is cards["Alastor"] # look up by id or by any name variant
True
The packaged snapshot ships with the wheel, so load_local() works offline (no
environment variable needed); translations and rulings are included. Online
tools should prefer load_online, which is more frequently updated.
Online loads are async and need an aiohttp session:
>>> import aiohttp, asyncio
>>> async def fetch_cards():
... async with aiohttp.ClientSession() as session:
... return await krcg.load_online(session)
>>> cards = asyncio.run(fetch_cards())
Completion and search
CardDict owns the name-completion and search index:
>>> [c.full_name for c in cards.complete("pentex")]
['Pentex™ Subversion',
'Pentex™ Loves You!',
'Enzo Giovanni, Pentex Board of Directors (G2)',
'Enzo Giovanni, Pentex Board of Directors (G2 ADV)',
'Harold Zettler, Pentex Director (G4)']
>>> # search takes keyword dimensions, each a list of values; returns a list of cards
>>> cards.search(type=["Political Action"], sect=["Anarch"], artist=["Drew Tucker"])
[100790|Free States Rant]
>>> cards.search(clan=["Banu Haqim"], title=["Justicar"])
[201598|Kasim Bayar, 201353|Tegyrius, Vizier (G2 ADV)]
>>> # the available dimensions and their possible values
>>> sorted(cards.search_dimensions)
['artist', 'bonus', 'capacity', 'city', 'clan', 'discipline', 'group', 'kind',
'path', 'precon', 'rarity', 'sect', 'set', 'title', 'trait', 'type']
>>> # values may include None (cards with no value in that dimension)
>>> [d for d in cards.search_dimensions["discipline"] if d][:5]
['ABO', 'ANI', 'AUS', 'CEL', 'CHI']
Within a dimension, multiple values are OR'd, except trait / discipline /
bonus, which intersect. Chain calls and combine the result lists for ANDs.
Text dimensions (name, card_text, flavor_text) do prefix search
and accept a lang (English, plus French/Spanish translations).
TWDA and decks
krcg.twda mirrors the cards loaders and returns a plain dict[str, Deck] keyed
by deck id (load() / load_local() / load_online(session)):
>>> from krcg import twda
>>> decks = twda.load()
>>> len(decks)
4538
>>> deck = decks["2019ecday2pf"]
>>> deck.name, deck.player
('Finnish Politics', 'Otso Saariluoma')
>>> deck.event.name, deck.event.date, deck.event.players_count
('EC 2019 - Day 2', datetime.date(2019, 8, 18), 50)
A Deck is a dataclass: its cards is a list[CardInDeck] (filter by card.kind),
plus metadata (name, author, player, comment, event, score).
Serialize it with krcg.providers - serialize_twd renders the full
TWD format and needs the cards handle, the others don't:
>>> from krcg import providers, Card
>>> sum(c.count for c in deck.cards if c.kind == Card.Kind.CRYPT)
12
>>> sum(c.count for c in deck.cards if c.kind == Card.Kind.LIBRARY)
65
>>> print(providers.serialize_twd(deck, cards))
EC 2019 - Day 2
Paris, France
August 18th 2019
3R+F
50 players
Otso Saariluoma
https://www.vekn.net/event-calendar/event/9327
-- 2GW8.5+1.5!
Deck Name: Finnish Politics
Crypt (12 cards, min=4, max=38, avg=5.75)
...
>>> providers.serialize_json_minimal(deck)["name"] # also: serialize_txt/vdb/lackey/jol
'Finnish Politics'
Parse a decklist from any text source with krcg.parser.deck_from_txt.
Pass twda=True for the positional TWDA tournament headers:
>>> from krcg import parser
>>> decklist = """\
... Deck Name: First Blood: Nosferatu
...
... Crypt (12 cards)
... 2x Gustaphe Brunnelle
... 2x Harold Tanner
... 2x Jeremy "Wix" Wyzchovsky
... 2x Petra
... 2x Beetleman
... 2x Benjamin Rose
...
... Library (12 cards)
... 2x Fame
... 2x Animalism
... 6x Aid from Bats
... 2x Cloak the Gathering
... """
>>> deck = parser.deck_from_txt(decklist.splitlines(), cards)
>>> deck.name
'First Blood: Nosferatu'
>>> sum(c.count for c in deck.cards if c.kind == Card.Kind.LIBRARY)
12
>>> # a file object works too: parser.deck_from_txt(open("deck.txt"), cards)
Fetch a deck from a supported site (Amaranth, VDB, VTESDecks)
with providers.fetch (async):
>>> async def grab(url):
... async with aiohttp.ClientSession() as session:
... return await providers.fetch(session, url, cards)
>>> deck = asyncio.run(
... grab("https://amaranth.vtes.co.nz/deck/4d3aa426-70da-44b7-8cb7-92377a1a0dbd")
... )
>>> deck.name
'First Blood: Tremere'
Analyzer
krcg.analyzer provides statistics and card affinity over any collection of
decks — the whole TWDA, a slice of it, or your own decks. Each function takes the
cards handle to resolve cards, so results are keyed by Card:
>>> from krcg import analyzer
>>> from datetime import date
>>> sample = [d for d in decks.values()
... if d.event and date(2019, 1, 1) < d.event.date < date(2020, 1, 1)]
>>> # how many of the decks play each card
>>> analyzer.played(sample, cards).most_common(3)
[(100588|Dreams of the Sphinx, 107),
(101384|Pentex™ Subversion, 101),
(101321|On the Qui Vive, 94)]
>>> # average and variance of the copies played, among decks that play the card
>>> analyzer.stats(sample, cards)[cards["Villein"]]
(4.441860465116283, 3.6884802595997837)
>>> # cards most often sharing a deck with a reference card
>>> # (similarity=1 restricts to decks that actually play it)
>>> analyzer.affinity(sample, cards, cards["Aid from Bats"], similarity=1)[:3]
[(100515|Deep Song, 1.0), (100301|Carrion Crows, 1.0), (101945|Taste of Vitae, 0.818)]
>>> # synthesize a TWDA-like deck around one or more seed cards
>>> built = analyzer.build_deck(sample, cards, cards["Aid from Bats"])
Seating
krcg.seating computes optimal tournament seatings against the nine official
VEKN criteria. Build the base rounds for your players, then optimise:
>>> from krcg import seating
>>> # the base rounds for 12 players over 3 rounds (handles the 6/7/11 edge cases)
>>> rounds = seating.get_rounds(list(range(1, 13)), 3)
>>> list(rounds[0].iter_tables())
[[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]]
>>> # pre-compute a full seating when attendance is fixed: fixed=1 keeps the
>>> # (arbitrary) first round and optimises the rest. The default (fixed=len-1)
>>> # only re-seats the last round — for building rounds one at a time as
>>> # attendance changes round to round.
>>> result, score = seating.optimise(rounds, iterations=50000, fixed=1)
>>> score.R1 # the mandatory predator-prey rule: no violations
[]
>>> score.rules # one value per rule R1..R9 (minor rules R4/R8/R9 may remain)
[0, 0, 0.0, 9, 0, 0, 0, 0.37, 2]
>>> help(seating.Score) # the full Score structure (R1..R9, deviations, totals)
Online / Offline usage
The online async loaders are designed for user-facing online tools. The static.krcg.org data source is refreshed daily from VEKN official repositories, so using online loaders combined with a daily reload ensures continued refresh of the data.
The offline loaders can be preferred for simple tools, scripts, or offline work. The packaged data is refreshed less frequently, only upon major VTES expansion releases.
Migrating from 4.x
5.0 is a hard break. The headline changes:
- No singletons.
krcg.vtes.VTES(the class and the module-level instance) is gone. Load aCardDictwithkrcg.load()/load_local()/load_online(session)and pass it where you need it. Likewisekrcg.twda.TWDAis gone —krcg.twda.load*()returns a plaindict[str, Deck]. - Search / complete moved onto the
CardDict:cards.search(...),cards.complete(...),cards.search_dimensions.searchreturns alist. - Deck operations are free functions, not methods:
VTES.parse(...)→parser.deck_from_txt(source, cards, ...)deck.to_txt("twd")→providers.serialize_twd(deck, cards)(andserialize_txt/serialize_vdb/serialize_lackey/serialize_jol/serialize_json_minimal)Deck.from_amaranth/from_vdb/from_vtesdecks(...)→await providers.fetch(session, url, cards)
- Models are plain dataclasses.
Deckis no longer acollections.Counter: its cards are alist[CardInDeck]— filter bycard.kindinstead of the olddeck.crypt/deck.libraryviews. Cards exposefull_name/unique_name/printed_name(there is no.name). - JSON:
Deck.to_json()/from_json()are gone — usemsgspec(msgspec.json.encode(deck)), orproviders.serialize_json_minimal(deck). - Network I/O is async (
aiohttp);load_online/fetchtake a session. - Offline mode no longer uses
LOCAL_CARDS—load_local()is the offline path. HTML scraping of the TWDA is gone (the archive ships bundled). - Renames:
seating.permutations→seating.get_rounds; theanalyzer.Analyzerclass → the free functionsplayed/stats/affinity/build_deck. - TWD scores now serialize in krcg's canonical form.
See the CHANGELOG for the full list.
Development
This project uses uv for dependency management and packaging, with just recipes for common tasks.
Setup
-
Install
uvif you haven't already:curl -LsSf https://astral.sh/uv/install.sh | sh
-
Clone the repository and install dependencies:
git clone https://github.com/lionel-panhaleux/krcg.git cd krcg uv sync --group dev
Development Commands
just quality- Run code quality checks (ruff check, ruff format --check, ty check)just test- Run tests (includes quality)just update- Update dependencies and sync external CSV/YAML datajust clean- Clean build artifactsjust sync-cards- Sync CSV files from vtescsv and rulings from vtes-biased/vtes-rulingsjust build- Build the packagejust release- Bump version (minor), tag, push, and publish to PyPI
Publishing uses uv publish and reads the token from a .pypi_token file at the repository root.
Contribute
Feel free to submit pull requests, they will be merged as long as they pass the tests. Do not hesitate to submit issues or vote on them if you want a feature implemented.
AI agents contributions
This repo includes a simple default AGENTS.md / CLAUDE.md harness. Make sure your AI agents use it when doing pull requests against this repo.
Design considerations
The package uses no database by design. The TWDA, search engine and cards dict are kept in memory for better performances. The whole library generates a memory footprint between 128MB and 256MB.
The package uses external data sources for card list, so that it needs not be updated when new sets are released or official VEKN CSV files are changed: it can use new data sets as soon as they're available.
Rulings
Rulings are sourced from the community-maintained repository
vtes-biased/vtes-rulings.
Please submit changes there. This library consumes those YAML files directly.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file krcg-5.2.tar.gz.
File metadata
- Download URL: krcg-5.2.tar.gz
- Upload date:
- Size: 1.9 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.19 {"installer":{"name":"uv","version":"0.11.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f92503f9a10409c3d567c181a0ac4c98b35d17c5630e9007ee61da265f0084b5
|
|
| MD5 |
aba98471b98a9ea6eaa105859dce9e19
|
|
| BLAKE2b-256 |
20deda5bbefcc1a6781156726fb921a4b53d8f73a2e54010aa3e6b7bc647ba3b
|
File details
Details for the file krcg-5.2-py3-none-any.whl.
File metadata
- Download URL: krcg-5.2-py3-none-any.whl
- Upload date:
- Size: 1.9 MB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.19 {"installer":{"name":"uv","version":"0.11.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
11a187092092fe3331e2309d0e7942e1dbe2bbd17ba9da4f269816ce7e3e1d73
|
|
| MD5 |
9596c1f626d51e27394323a468949288
|
|
| BLAKE2b-256 |
7c26ae28e2039086a5fa610b538dc0aab9630153cddc5b2639263cf6b92a10c3
|