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.0.tar.gz (34.6 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.0-py3-none-any.whl (22.3 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: gungi-0.1.0.tar.gz
  • Upload date:
  • Size: 34.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.8

File hashes

Hashes for gungi-0.1.0.tar.gz
Algorithm Hash digest
SHA256 b1a98e1d251216a5e493fd9613d8501a41bf5fc97b8e0740248522794b670a54
MD5 3b9b698ffd5edb95eb75b056234005de
BLAKE2b-256 dbb138a4276dec3b0f1d76d73485865062051ecdf0e61f5dd42c18a9bf95d70f

See more details on using hashes here.

File details

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

File metadata

  • Download URL: gungi-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 22.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.8

File hashes

Hashes for gungi-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 13cd7765165abc7ba69ff2d1ec7befc3b428d0cb32182cc88bd5b13a376b41af
MD5 3958e36633f4b7cea1f4285acde55463
BLAKE2b-256 3ba40243d4f7d1964c99d2fb9a6b72c13696763b4e0f7093845a814e78ecb75b

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