Metadata-Version: 2.4
Name: gungi
Version: 0.1.0
Summary: Python port of the gungi.js library with verified cross-language parity
License-Expression: MIT
Project-URL: Homepage, https://github.com/francislabountyjr/gungi.py
Project-URL: Repository, https://github.com/francislabountyjr/gungi.py
Project-URL: Issues, https://github.com/francislabountyjr/gungi.py/issues
Project-URL: Source-of-truth, https://github.com/jwyce/gungi.js
Keywords: gungi,board-game,engine,python-port,game-rules
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: dev
Requires-Dist: build>=1.2.2; extra == "dev"
Requires-Dist: pytest>=8.3.0; extra == "dev"
Requires-Dist: ruff>=0.6.9; extra == "dev"
Dynamic: license-file

# gungi.py

[![Python Quality](https://github.com/francislabountyjr/gungi.py/actions/workflows/python-quality.yml/badge.svg)](https://github.com/francislabountyjr/gungi.py/actions/workflows/python-quality.yml)
[![Gungi.js Parity](https://github.com/francislabountyjr/gungi.py/actions/workflows/gungi-js-parity.yml/badge.svg)](https://github.com/francislabountyjr/gungi.py/actions/workflows/gungi-js-parity.yml)
[![Weekly Gungi.js Sync](https://github.com/francislabountyjr/gungi.py/actions/workflows/weekly-gungi-js-sync.yml/badge.svg)](https://github.com/francislabountyjr/gungi.py/actions/workflows/weekly-gungi-js-sync.yml)

[Original JS project](https://github.com/jwyce/gungi.js) 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

```bash
pip install gungi
```

Or from this repo:

```bash
pip install -e .
```

## Development

```bash
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`:

```bash
python scripts/check_gungi_js_parity.py
```

For a heavier randomized differential run:

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

## Example Code

Random game:

```python
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

```python
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)`

```python
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)`

```python
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()`

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

### `.captured(color)`

```python
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()`

```python
gungi.clear()
print(gungi.fen())
```

### `.fen()`

```python
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)`

```python
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)`

```python
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)`

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

### `.hand(color)`

```python
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)`

```python
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()`

```python
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()`

```python
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()`

```python
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)`

```python
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)`

```python
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:

```python
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:

```python
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()`

```python
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(...)`

```python
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)`

```python
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()`

```python
gungi.print()
```

### `.reset()`

```python
gungi.reset()
```

### `.turn()`

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

### `.undo()`

```python
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)`

```python
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"))
```
