Skip to main content

Python port of the gungi.js library with verified cross-language parity

Project description

gungi.py

Python Quality Gungi.js Parity Weekly Gungi.js Sync

Original JS project ported to Python. It implements the official Gungi rules shown in HUNTER×HUNTER: move generation/validation, piece placement/movement, drafting, captures, betrayal, arata, and endgame detection.

Installation

pip install gungi

Or from this repo:

pip install -e .

Development

pip install -e .[dev]
ruff check .
ruff format .
pytest
python -m build

Parity Verification

gungi.js is the source of truth for engine behavior. The verified upstream commit is recorded in gungi_js_commit_hash.txt.

Run the local cross-language parity suite against an adjacent ../gungi.js checkout or by setting GUNGI_JS_DIR:

python scripts/check_gungi_js_parity.py

For a heavier randomized differential run:

python scripts/check_gungi_js_parity.py --parity-cases-per-mode 5 --parity-plies 40

Example Code

Random game:

from gungi import ADVANCED_POSITION, Gungi


def clear_terminal() -> None:
    print("\033[2J\033[H", end="")


def print_text(text: str) -> None:
    clear_terminal()
    print(text)


gungi = Gungi(ADVANCED_POSITION)

while not gungi.is_game_over():
    moves = gungi.moves()
    move = moves[len(moves) // 2]
    gungi.move(move)
    print_text(gungi.ascii())

print(gungi.fen() + "\n")
print(gungi.pgn())

API

Constants

from gungi import (
    WHITE, BLACK,
    TSUKE, TAKE, BETRAY, ARATA,
    MARSHAL, GENERAL, LIEUTENANT_GENERAL, MAJOR_GENERAL, WARRIOR, LANCER,
    RIDER, SPY, FORTRESS, SOLDIER, CANNON, ARCHER, MUSKETEER, TACTICIAN,
    INTRO_POSITION, BEGINNER_POSITION, INTERMEDIATE_POSITION, ADVANCED_POSITION,
    SQUARES, CANONICAL_NAMES, ENGLISH_NAMES, FEN_CODES,
)

Constructor: Gungi(fen: str | None = None)

from gungi import Gungi

# default (INTRO_POSITION)
gungi = Gungi()

# load a FEN
gungi = Gungi(
    "d1f1|r:d||r:j|j|k:a:c||g:s|/3|w:s|2|n:w|2/f8/3n2d1m/2i4W1/3dN1tR1/4D2R1/ASJ2|C:a|1A1/|I:F:W|F1SGMT1|K:J| -/- w 3 - 57"
)

.ascii(english: bool = False)

from gungi import BEGINNER_POSITION, Gungi

gungi = Gungi(BEGINNER_POSITION)
gungi.move("槍(8-5-1)(7-5-2)付")
gungi.move("新馬(1-7-1)")
gungi.move("新忍(8-7-2)付")

print(gungi.ascii())
print(gungi.ascii(english=True))

.board()

gungi = Gungi()
print(gungi.board())

.captured(color)

from gungi import BEGINNER_POSITION, Gungi

gungi = Gungi(BEGINNER_POSITION)
gungi.move("新少(7-5-2)付")
gungi.move("兵(3-5-1)(4-5-1)")
gungi.move("新槍(7-4-2)付")
gungi.move("兵(4-5-1)(5-5-1)")
gungi.move("少(7-5-2)取-(5-5-1)")

print(gungi.captured("b"))

.clear()

gungi.clear()
print(gungi.fen())

.fen()

gungi = Gungi(BEGINNER_POSITION)
gungi.move("槍(8-5-1)(7-5-2)付")
gungi.move("新馬(1-7-1)")
gungi.move("新忍(8-7-2)付")
print(gungi.fen())

.get(square)

gungi = Gungi(BEGINNER_POSITION)
gungi.move("砦(7-3-1)(7-4-2)付")
print(gungi.get("7-4"))
print(gungi.get("7-3"))

.get_drafting_rights(color)

from gungi import ADVANCED_POSITION, Gungi

gungi = Gungi(ADVANCED_POSITION)
gungi.move("新帥(7-2-1)終")
print(gungi.get_drafting_rights())
print(gungi.get_drafting_rights("b"))

.get_top(square)

gungi = Gungi(BEGINNER_POSITION)
print(gungi.get_top("7-4"))
print(gungi.get_top("7-3"))

.hand(color)

gungi = Gungi(BEGINNER_POSITION)
gungi.move("槍(8-5-1)(7-5-2)付")
gungi.move("新馬(1-7-1)")
gungi.move("新忍(8-7-2)付")

print(gungi.hand())
print(gungi.hand("w"))

.history(verbose: bool = False)

gungi = Gungi(BEGINNER_POSITION)
gungi.move("新少(7-5-2)付")
gungi.move("兵(3-5-1)(4-5-1)")
gungi.move("新槍(7-4-2)付")
gungi.move("兵(4-5-1)(5-5-1)")
gungi.move("少(7-5-2)取-(5-5-1)")

print(gungi.history())
print(gungi.history(verbose=True))

.in_draft()

gungi = Gungi(ADVANCED_POSITION)
gungi.move("新帥(7-2-1)終")
print(gungi.in_draft())
gungi.move("新帥(2-4-1)終")
print(gungi.in_draft())

.is_fourfold_repetition()

from gungi import INTRO_POSITION, Gungi

gungi = Gungi(INTRO_POSITION)
print(gungi.is_fourfold_repetition())

gungi.move("兵(7-1-1)(6-1-1)")
gungi.move("侍(3-4-1)(4-4-1)")
gungi.move("兵(6-1-1)(7-1-1)")
gungi.move("侍(4-4-1)(3-4-1)")

gungi.move("兵(7-1-1)(6-1-1)")
gungi.move("侍(3-4-1)(4-4-1)")
gungi.move("兵(6-1-1)(7-1-1)")
gungi.move("侍(4-4-1)(3-4-1)")

gungi.move("兵(7-1-1)(6-1-1)")
gungi.move("侍(3-4-1)(4-4-1)")
gungi.move("兵(6-1-1)(7-1-1)")
gungi.move("侍(4-4-1)(3-4-1)")

print(gungi.is_fourfold_repetition())

.is_game_over()

from gungi import ADVANCED_POSITION, Gungi

gungi = Gungi(ADVANCED_POSITION)
print(gungi.is_game_over())

gungi.load("1|g:N|2|W:N|Ad1f/7r1/1nd2Adfr/2|c:G|j2K2/6s1D/1w|W:T|6/2F4J|F:D|/i8/2|S:w||R:M|3C1 -/- b 3 - 164")
print(gungi.is_game_over())

.load(fen)

gungi = Gungi()
gungi.load("d1f1|r:d||r:j|j|k:a:c||g:s|/3|w:s|2|n:w|2/f8/3n2d1m/2i4W1/3dN1tR1/4D2R1/ASJ2|C:a|1A1/|I:F:W|F1SGMT1|K:J| -/- w 3 - 57")

try:
    gungi.load("3img3/1ra1n1xas1/d1fwdwf1d/9/9/9/9/9 J2N2R1D1/j2n2r2d1 w 1 - 1")
except Exception as e:
    print(e)

.load_pgn(pgn, fen=ADVANCED_POSITION, opts=None)

from gungi import ADVANCED_POSITION, Gungi, PGNOptions

gungi = Gungi(ADVANCED_POSITION)
pgn = [
    "1.新帥(7-9-1)終 新帥(1-5-1) 新少(2-8-1) 新謀(1-2-1) 新馬(2-8-2)付 新将(3-4-1)終",
    "2.帥(7-9-1)(8-8-1) 新砦(3-2-1) 3.新謀(8-2-1) 新弓(1-8-1) 4.新弓(9-9-1) 新馬(1-3-1)",
    "5.新筒(9-5-1) 新弓(1-8-2)付 6.新兵(8-5-1) 新砦(3-6-1) 7.新将(8-9-1) 砦(3-6-1)(2-5-1)",
    "8.新少(8-9-2)付 新少(1-7-1) 9.新槍(8-2-2)付 新侍(1-2-2)付 10.新兵(9-8-1) 少(1-7-1)(2-7-1)",
    "11.新槍(8-6-1) 新侍(2-4-1) 12.新砦(9-9-2)付 侍(2-4-1)(3-3-1) 13.少(8-9-2)(7-8-1) 新兵(1-4-1)",
]
gungi.load_pgn("\n".join(pgn), opts=PGNOptions(newline="\n"))
gungi.print()

.move(move)

SAN:

from gungi import INTRO_POSITION, Gungi

gungi = Gungi(INTRO_POSITION)
print(gungi.move("兵(7-1-1)(6-1-1)"))

gungi.move("将(1-4-1)(2-10-1)")  # raises

Object:

from gungi import Gungi, SOLDIER

gungi = Gungi(INTRO_POSITION)
print(gungi.move({"piece": SOLDIER, "from_": "7-1-1", "to": "6-1-1", "type": "route"}))

.move_number()

from gungi import Gungi

gungi = Gungi()
gungi.load("d1f1|r:d||r:j|j|k:a:c||g:s|/3|w:s|2|n:w|2/f8/3n2d1m/2i4W1/3dN1tR1/4D2R1/ASJ2|C:a|1A1/|I:F:W|F1SGMT1|K:J| -/- w 3 - 57")
print(gungi.move_number())

.moves(...)

from gungi import INTRO_POSITION, Gungi

gungi = Gungi(INTRO_POSITION)
print(gungi.moves())
print(gungi.moves(square="8-5"))
print(gungi.moves(arata=gungi.hand("w")[0]))
print(gungi.moves(verbose=True))

.pgn(opts=None)

from gungi import BEGINNER_POSITION, Gungi, PGNOptions

gungi = Gungi(BEGINNER_POSITION)
gungi.move("槍(8-5-1)(7-5-2)付")
gungi.move("新馬(1-7-1)")
gungi.move("新忍(8-7-2)付")

print(gungi.pgn(PGNOptions(max_width=2, newline="<br />")))

.print()

gungi.print()

.reset()

gungi.reset()

.turn()

gungi.load("3img3/1s2n2s1/d1fwdwf1d/9/9/9/D1FW|D:J|WF1D/1S2N2S1/3GMI3 J1N2R2D1/j2n2r2d1 b 0 - 1")
print(gungi.turn())

.undo()

from gungi import BEGINNER_POSITION, Gungi

gungi = Gungi(BEGINNER_POSITION)
print(gungi.fen())
gungi.move("兵(7-1-1)(6-1-1)")
print(gungi.fen())
print(gungi.undo())
print(gungi.fen())
print(gungi.undo())

validate_fen(fen)

from gungi import validate_fen

print(validate_fen("3img3/1s2n2s1/d1fw1wf1d/9/4J4/9/D1FWD|W:N|F1D/1S2N2S1/3GMI3 J1N1R2D1/j2n2r2d1 b 0 - 3"))
print(validate_fen("3img3/1ra1n1xas1/d1fwdwf1d/9/9/9/9/9/9 J2N2R1D1/j2n2r2d1 w 1 - 1"))

Project details


Download files

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

Source Distribution

gungi-0.1.1.tar.gz (33.8 kB view details)

Uploaded Source

Built Distribution

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

gungi-0.1.1-py3-none-any.whl (22.3 kB view details)

Uploaded Python 3

File details

Details for the file gungi-0.1.1.tar.gz.

File metadata

  • Download URL: gungi-0.1.1.tar.gz
  • Upload date:
  • Size: 33.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for gungi-0.1.1.tar.gz
Algorithm Hash digest
SHA256 941305714107a410f99e8dc3bb9ccaa713b06e8456fde6b336a90cce6db62356
MD5 631fe5793c084af854942af78560e61b
BLAKE2b-256 7f21b9e56cb9d59bff15119bfd500ea7a1ed63932d749cb879319218ea89b85e

See more details on using hashes here.

Provenance

The following attestation bundles were made for gungi-0.1.1.tar.gz:

Publisher: python-publish.yml on francislabountyjr/gungi.py

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file gungi-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: gungi-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 22.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for gungi-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 3d0144f4fc9233f789c9bb723b074756f952cb911a920150d8ecc89f0c64f3e3
MD5 8562995b3eb712113a920ce3b43c8684
BLAKE2b-256 10aa9a282562137f07554f3e189e07eb071ca6f27ee38018b5bc3a2874d6e0b3

See more details on using hashes here.

Provenance

The following attestation bundles were made for gungi-0.1.1-py3-none-any.whl:

Publisher: python-publish.yml on francislabountyjr/gungi.py

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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