Skip to content

Commit

Permalink
Add ability to visualize the board as SVG (#47)
Browse files Browse the repository at this point in the history
* Add files via upload

* Update setup.cfg

* Add files via upload

* Add files via upload

* Add files via upload

* Add files via upload

* Update test_pdn.py

* Update test_pdn.py

* Update test_engines.py
  • Loading branch information
AttackingOrDefending authored Jan 12, 2025
1 parent af29e2f commit 7534d51
Show file tree
Hide file tree
Showing 8 changed files with 398 additions and 6 deletions.
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# pydraughts
[![PyPI version](https://badge.fury.io/py/pydraughts.svg)](https://badge.fury.io/py/pydraughts) [![Tests](https://github.com/AttackingOrDefending/pydraughts/actions/workflows/tests.yml/badge.svg)](https://github.com/AttackingOrDefending/pydraughts/actions/workflows/tests.yml) [![Build](https://github.com/AttackingOrDefending/pydraughts/actions/workflows/build.yml/badge.svg)](https://github.com/AttackingOrDefending/pydraughts/actions/workflows/build.yml) [![CodeQL](https://github.com/AttackingOrDefending/pydraughts/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/AttackingOrDefending/pydraughts/actions/workflows/codeql-analysis.yml) [![codecov](https://codecov.io/gh/AttackingOrDefending/pydraughts/branch/main/graph/badge.svg?token=ZSPXIVSAWN)](https://codecov.io/gh/AttackingOrDefending/pydraughts)

pydraughts is a draughts (checkers) library for Python with move generation, PDN reading and writing, engine communication and balloted openings. It is based on [ImparaAI/checkers](https://github.com/ImparaAI/checkers).
pydraughts is a draughts (checkers) library for Python with move generation, SVG visualizations, PDN reading and writing, engine communication and balloted openings. It is based on [ImparaAI/checkers](https://github.com/ImparaAI/checkers).

Installing
----------
Expand Down Expand Up @@ -48,7 +48,13 @@ board.push(move)
board2 = Board(fen="W:WK40:B19,29")
board2.push(Move(board2, pdn_move='40x14'))
```
* Get a visual representation of the board
* Get a visual representation of the board as SVG
```python
from draughts import svg
svg.create_svg(Board(fen="B:W16,19,33,34,47,K4:B17,25,26"))
```
![SVG Board](examples/board.svg)
* Get a visual representation of the board in the terminal
```python
print(board)

Expand Down
4 changes: 2 additions & 2 deletions draughts/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
__all__ = ["Board", "Move", "WHITE", "BLACK"]

__author__ = "Ioannis Pantidis"
__copyright__ = "2021-2024, " + __author__
__version__ = "0.6.6"
__copyright__ = "2021-2025, " + __author__
__version__ = "0.6.7"
94 changes: 94 additions & 0 deletions draughts/svg.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import math
from draughts import Board


def create_svg(board: Board) -> str:
"""Create an SVG of a board."""
# Base square size
square_size = 40
margin = 16 # Fixed margin size for coordinates

# Calculate SVG dimensions based on board size
str_representation = list(map(lambda row_str: row_str.split("|"), filter(lambda row_str: "|" in row_str, str(board).split("\n"))))
width = len(str_representation[0])
height = len(str_representation)
svg_width = (square_size * width) + (2 * margin)
svg_height = (square_size * height) + (2 * margin)

# Background color for coordinates
svg = [f'''<svg viewBox="0 0 {svg_width} {svg_height}" xmlns="http://www.w3.org/2000/svg">
<rect x="0" y="0" width="{svg_width}" height="{svg_height}" fill="#1A1A1A"/>''']

# Add coordinates in white
for i in range(width):
# Letters along bottom
svg.append(f'<text x="{margin + i * square_size + square_size / 2}" '
f'y="{svg_height - margin / 4}" '
f'text-anchor="middle" font-size="{margin * 0.8}" '
f'fill="white">{chr(97 + i)}</text>')

for i in range(height):
# Numbers along left side
svg.append(f'<text x="{margin / 2}" '
f'y="{margin + i * square_size + square_size / 2}" '
f'text-anchor="middle" dominant-baseline="central" '
f'font-size="{margin * 0.8}" fill="white">{height - i}</text>')

# Draw board
for row in range(height):
for col in range(width):
x = margin + col * square_size
y = margin + row * square_size
color = "#E8D0AA" if (row + col) % 2 == 0 else "#B87C4C"
svg.append(f'<rect x="{x}" y="{y}" width="{square_size}" '
f'height="{square_size}" fill="{color}"/>')

# Draw pieces
for row, row_str in enumerate(str_representation):
for col, piece in enumerate(row_str):
piece = piece.strip()
if not piece:
continue

# Center of square
cx = margin + col * square_size + square_size // 2
cy = margin + row * square_size + square_size // 2

piece_radius = square_size * 0.4
piece_color = "#000000" if piece.lower() == 'b' else "#FFFFFF"
stroke_color = "#FFFFFF" if piece.lower() == 'b' else "#000000"

# Draw main piece
svg.append(f'<circle cx="{cx}" cy="{cy}" r="{piece_radius}" '
f'fill="{piece_color}" stroke="{stroke_color}" stroke-width="2"/>')

# Enhanced crown for kings
if piece.isupper():
gradient_id = f"crown_gradient_{cx}_{cy}"
svg.append(f'''<defs>
<linearGradient id="{gradient_id}" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#FFD700;stop-opacity:1" />
<stop offset="50%" style="stop-color:#FFA500;stop-opacity:1" />
<stop offset="100%" style="stop-color:#FFD700;stop-opacity:1" />
</linearGradient>
</defs>''')

# Draw 5-pointed star
num_points = 5
outer_radius = piece_radius * 0.5
inner_radius = outer_radius * 0.382
points = []

for i in range(num_points * 2):
angle = (i * math.pi / num_points) - (math.pi / 2)
radius = outer_radius if i % 2 == 0 else inner_radius
x = cx + radius * math.cos(angle)
y = cy + radius * math.sin(angle)
points.append(f"{x},{y}")

svg.append(f'<path d="M {" L ".join(points)} Z" '
f'fill="url(#{gradient_id})" '
f'stroke="#DAA520" stroke-width="2"/>')

svg.append('</svg>')
return '\n'.join(svg)
140 changes: 140 additions & 0 deletions examples/board.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name = pydraughts
version = attr: draughts.__version__
author = Ioannis Pantidis
description = A draughts library for Python with move generation, PDN reading and writing, engine communication and balloted openings
description = A draughts library for Python with move generation, SVG visualizations, PDN reading and writing, engine communication and balloted openings
long_description = file: README.md
long_description_content_type = text/markdown
keywords = checkers, draughts, game, fen, pdn
Expand Down
4 changes: 4 additions & 0 deletions test_pydraughts/test_engines.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ def test_checkerboard_engines():
break
logger.info('Finished playing 1')
checkerboard.kill_process()
logger.info('Closed engines 1')

checkerboard = CheckerBoardEngine('cake_189f.dll')
limit = Limit(10)
Expand All @@ -225,6 +226,7 @@ def test_checkerboard_engines():
break
logger.info('Finished playing 2')
checkerboard.kill_process()
logger.info('Closed engines 2')


@pytest.mark.timeout(300, method="thread")
Expand All @@ -245,6 +247,7 @@ def test_russian_checkerboard_engines():
break
logger.info('Finished playing 1')
checkerboard.kill_process()
logger.info('Closed engines 1')

checkerboard = CheckerBoardEngine('kestog.dll')
limit = Limit(10)
Expand All @@ -259,6 +262,7 @@ def test_russian_checkerboard_engines():
break
logger.info('Finished playing 2')
checkerboard.kill_process()
logger.info('Closed engines 2')


@pytest.mark.timeout(450, method="thread")
Expand Down
2 changes: 1 addition & 1 deletion test_pydraughts/test_pdn.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

def download_games():
headers = {'User-Agent': 'User Agent', 'From': 'mail@mail.com'}
response = requests.get('https://pdn.fmjd.org/_downloads/games.zip', headers=headers, allow_redirects=True)
response = requests.get('https://github.com/wiegerw/pdn/raw/refs/heads/master/games/games.zip', headers=headers, allow_redirects=True)
with open('./TEMP/games.zip', 'wb') as file:
file.write(response.content)
with zipfile.ZipFile('./TEMP/games.zip', 'r') as zip_ref:
Expand Down
Loading

0 comments on commit 7534d51

Please sign in to comment.