Skip to main content

Library for VTES cards and TWDA.

Project description

KRCG

PyPI version Validation Python version License Code Quality

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.

Dark Pack

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 a CardDict with krcg.load() / load_local() / load_online(session) and pass it where you need it. Likewise krcg.twda.TWDA is gone — krcg.twda.load*() returns a plain dict[str, Deck].
  • Search / complete moved onto the CardDict: cards.search(...), cards.complete(...), cards.search_dimensions. search returns a list.
  • Deck operations are free functions, not methods:
    • VTES.parse(...)parser.deck_from_txt(source, cards, ...)
    • deck.to_txt("twd")providers.serialize_twd(deck, cards) (and serialize_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. Deck is no longer a collections.Counter: its cards are a list[CardInDeck] — filter by card.kind instead of the old deck.crypt / deck.library views. Cards expose full_name / unique_name / printed_name (there is no .name).
  • JSON: Deck.to_json() / from_json() are gone — use msgspec (msgspec.json.encode(deck)), or providers.serialize_json_minimal(deck).
  • Network I/O is async (aiohttp); load_online / fetch take a session.
  • Offline mode no longer uses LOCAL_CARDSload_local() is the offline path. HTML scraping of the TWDA is gone (the archive ships bundled).
  • Renames: seating.permutationsseating.get_rounds; the analyzer.Analyzer class → the free functions played / 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

  1. Install uv if you haven't already:

    curl -LsSf https://astral.sh/uv/install.sh | sh
    
  2. 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 data
  • just clean - Clean build artifacts
  • just sync-cards - Sync CSV files from vtescsv and rulings from vtes-biased/vtes-rulings
  • just build - Build the package
  • just 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

This version

5.6

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

krcg-5.6.tar.gz (1.9 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

krcg-5.6-py3-none-any.whl (1.9 MB view details)

Uploaded Python 3

File details

Details for the file krcg-5.6.tar.gz.

File metadata

  • Download URL: krcg-5.6.tar.gz
  • Upload date:
  • Size: 1.9 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.26 {"installer":{"name":"uv","version":"0.11.26","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

Hashes for krcg-5.6.tar.gz
Algorithm Hash digest
SHA256 43da4a99a66d8998154e8ed3784f05facb04cec1fe8315b8bcc5de3f54697987
MD5 9a6ce57e6c981a66b0d823e84a79de55
BLAKE2b-256 35a5626e89c6328471c33fda136d4227c1b73613529b28ae4edf3ef07713b292

See more details on using hashes here.

File details

Details for the file krcg-5.6-py3-none-any.whl.

File metadata

  • Download URL: krcg-5.6-py3-none-any.whl
  • Upload date:
  • Size: 1.9 MB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.26 {"installer":{"name":"uv","version":"0.11.26","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

Hashes for krcg-5.6-py3-none-any.whl
Algorithm Hash digest
SHA256 9a19c1fd0e76f34347eb418da332a443418b4a31914632b853ead5fd85bdb0cf
MD5 74b2b0285eea497b55d695b15426fa33
BLAKE2b-256 b958493f63e03abe29b2e4b6775d92441202bfb974da1c12a7d2e5609c5e5c0d

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page