[Python] How to manage multiple chess notations while preserving a single source of truth and...

Discussão em 'Python' iniciado por Stack, Junho 26, 2025.

  1. Stack

    Stack Membro Participativo

    I'm building a chess engine in Python. A recurring design challenge is the growing number of notations I need to support:

    • LAN (Long Algebraic Notation), e.g. "e2e4"
    • SAN (Standard Algebraic Notation), e.g. "Nf3"
    • UCI, abbreviated algebraic
    • a custom internal format

    Each notation expresses a different amount of information. For example, SAN includes disambiguation, promotion, and check/mate indicators, which depend on the board state.

    As an analogy, imagine you're building a translation engine between multiple human languages (e.g. English, French, Spanish), but there’s no common root or “universal” language. Some languages express concepts that others omit or express differently. Likewise, each chess notation represents a move in a different way, sometimes embedding more context or requiring position-specific interpretation.
    I think my problem is similar to managing multiple translation systems without drifting away from a consistent, central meaning.


    I use a class Move as the canonical internal representation. It is deliberately kept free from business logic like legality, check, or mate.

    It stores:

    • origin and destination squares
    • moving piece (pieceMoved)
    • piece captured at destination (pieceCaptured)
    • optional promotion piece
    • flags for special moves (e.g. castling, en passant)

    A Move is always built from a board state and coordinates.
    Even if constructed from minimal data, it always reflects the actual content of the origin and target squares at creation time (even if empty).


    In my codebase, I distinguish two creation modes:


    • A raw Move is built from coordinates and a basic board layout (list[list[str]]), without checking legality.
      It reflects a mechanical displacement from one square to another.
      It can produce incoherent or illegal moves, but still records accurate pieceMoved and pieceCaptured from the given layout.


    • A valid Move is created by resolving a LAN string against a full GameState (including turn, rights, en passant, etc.).
      It guarantees legality and completeness under the rules of chess.

    To parse external LAN input, I use:

    • LANtoRawMove(lan: str, board: list[list[str]]) -> Move
    • LANtoValidMove(lan: str, state: GameState) -> Move

    These functions are mainly used for parsing files and test setup, not core engine logic.


    To facilitate test composition, I use two fixtures:


    • raw_fixt:
      Takes a list of LAN strings and a board snapshot.
      Uses LANtoRawMove to generate moves, applies them in sequence, and returns the resulting board and list of Moves.


    • valid_fixt:
      Takes a list of LAN strings and a GameState.
      Uses LANtoValidMove, applies the moves sequentially, and returns the resulting game state and list of valid Moves.
      It assumes that each LAN string corresponds to exactly one valid move at each step.

    I considered turning these fixtures into general-purpose utilities (e.g. raw_generate()) to use them outside tests, but that doesn’t seem to affect the underlying issues.

    Main question


    Is it acceptable to test a function using a fixture that depends on it?
    For example, can I test LANtoRawMove using raw_fixt, even though raw_fixt calls it internally?

    Secondary questions

    • Are there best practices or design patterns to avoid validation loops or preserve a clear single source of truth in this kind of test setup?
    • Are there tools or techniques to detect hidden dependencies or indirect test coupling?
    • More broadly, in a system with multiple notations of varying semantic weight, is the concept of a single source of truth still meaningful or viable?

    Thanks in advance. Let me know if any clarification is needed.

    Continue reading...

Compartilhe esta Página