gungi.py
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"))
Release files for gungi 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| gungi-0.1.1.tar.gz | 33.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| gungi-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 56.1 kB
Release files / gungi-0.1.1.tar.gz
| Download URL | gungi-0.1.1.tar.gz |
|---|---|
| Size | 33.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
941305714107a410f99e8dc3bb9ccaa713b06e8456fde6b336a90cce6db62356
|
|
BLAKE2b-256 checksum How to use checksums |
7f21b9e56cb9d59bff15119bfd500ea7a1ed63932d749cb879319218ea89b85e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Mar 7, 2026.
Transparency logRelease files / gungi-0.1.1-py3-none-any.whl
| Download URL | gungi-0.1.1-py3-none-any.whl |
|---|---|
| Size | 22.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3d0144f4fc9233f789c9bb723b074756f952cb911a920150d8ecc89f0c64f3e3
|
|
BLAKE2b-256 checksum How to use checksums |
10aa9a282562137f07554f3e189e07eb071ca6f27ee38018b5bc3a2874d6e0b3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Mar 7, 2026.
Transparency log