diff --git a/README.rst b/README.rst index ac059b1..c24f7d2 100644 --- a/README.rst +++ b/README.rst @@ -105,6 +105,21 @@ Features +---------------------------+ 先手の持駒: 銀 +* Render board as SVG. + + .. code:: python + + >>> print(board.svg()) + + ... + + >>> with open('board.svg', 'w') as tfile: + ... print >> tfile, board.svg() # python 2.7 + ... print(board.svg(), file=tfile) # python 3+ + + .. image:: data/images/board.svg + :align: center + * Detects checkmates, stalemates. .. code:: python diff --git a/data/images/board.svg b/data/images/board.svg new file mode 100644 index 0000000..1afa36b --- /dev/null +++ b/data/images/board.svg @@ -0,0 +1,296 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + > + + > + + > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 9 + 8 + 7 + 6 + 5 + 4 + 3 + 2 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/shogi/SVG.py b/shogi/SVG.py new file mode 100644 index 0000000..b9695f7 --- /dev/null +++ b/shogi/SVG.py @@ -0,0 +1,383 @@ +# -*- coding: utf-8 -*- +# +# This file is part of the python-shogi library. +# +# Copyright (C) 2020- Yui Matsumura +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +def sfen_to_svg(sfen): + svg = """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + > + + > + + > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 9 + 8 + 7 + 6 + 5 + 4 + 3 + 2 + 1 + + + + + + + + + + + + + + + + + + """ + + reftable = { + 'l': 'white-lance', + 'n': 'white-knight', + 's': 'white-silver', + 'g': 'white-gold', + 'k': 'white-king', + 'r': 'white-rook', + 'b': 'white-bishop', + 'p': 'white-pawn', + 'L': 'black-lance', + 'N': 'black-knight', + 'S': 'black-silver', + 'G': 'black-gold', + 'K': 'black-king', + 'R': 'black-rook', + 'B': 'black-bishop', + 'P': 'black-pawn' + } + + reftablepromote = { + 'l': 'white-pro-lance', + 'n': 'white-pro-knight', + 's': 'white-pro-silver', + 'r': 'white-dragon', + 'b': 'white-horse', + 'p': 'white-pro-pawn', + 'L': 'black-pro-lance', + 'N': 'black-pro-knight', + 'S': 'black-pro-silver', + 'R': 'black-dragon', + 'B': 'black-horse', + 'P': 'black-pro-pawn' + } + + # render pieces on board + sfen_conf = sfen.split()[0].split('/') + board = [] + promoted = False + + for row_index, row in enumerate(sfen_conf): + colnum = 1 + for col in row: + if col.isdecimal(): + colnum += int(col) + elif col == '+': + promoted = True + else: + x = (colnum) * 20 + y = ((row_index + 1) * 20) - 10 + if promoted: + ref = reftablepromote[col] + else: + ref = reftable[col] + t = '' + use = t.format(ref, x, y) + board.append(use) + colnum += 1 + promoted = False + + # render owned pieces + sfen_ownp = sfen.split()[2] + ownp_idx_WHITE = 0 + ownp_idx_BLACK = 0 + quantity = 1 + board.append('') + + if not sfen_ownp == '-': + for p in sfen_ownp: + if p.isdecimal(): + quantity = ''.join((quantity, p)) + continue + + if not p.islower(): + if quantity: + quantity = int(quantity) + else: + quantity = 1 + x = 212 + y = 115 - (ownp_idx_WHITE*18) + ref = reftable[p] + t = '' + use = t.format(ref, x, y) + ownp_idx_WHITE += 1 + board.append(use) + if quantity > 1: + t = '{}' + text = t.format((x+2), (y+7), quantity) + board.append(text) + else: + if quantity: + quantity = int(quantity) + else: + quantity = 1 + x = 0 + y = 65 + (ownp_idx_BLACK*18) + ref = reftable[p] + t = '' + use = t.format(ref, x, y) + ownp_idx_BLACK += 1 + board.append(use) + if quantity > 1: + t = '{}' + text = t.format(x, (y+16), quantity) + board.append(text) + quantity = '' + + svg = svg+'\n'.join(board)+'\n' + return svg \ No newline at end of file diff --git a/shogi/__init__.py b/shogi/__init__.py index 992964b..57d0687 100644 --- a/shogi/__init__.py +++ b/shogi/__init__.py @@ -28,6 +28,8 @@ from .Move import * from .Piece import * from .Consts import * +from .SVG import * + PIECE_TYPES_WITHOUT_KING = [ PAWN, LANCE, KNIGHT, SILVER, @@ -1124,6 +1126,10 @@ def sfen(self): return ''.join(sfen) + def svg(self): + svg = SVG.sfen_to_svg(self.sfen()) + return svg + def set_sfen(self, sfen): ''' Parses a SFEN and sets the position from it. diff --git a/tests/svg_test.py b/tests/svg_test.py new file mode 100644 index 0000000..b10148d --- /dev/null +++ b/tests/svg_test.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +# +# This file is part of the python-shogi library. +# +# Copyright (C) 2020- Yui Matsumura +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import shogi +import unittest + + +class BoardTestCase(unittest.TestCase): + def test_default_board(self): + board = shogi.Board() + svg_file = board.svg() + pieces = svg_file.split('')[1] + board_positions = pieces.split('')[0] + hand_positions = pieces.split('')[1] + + # check board pieces + self.assertTrue(board_positions.count('bishop') == 2) + self.assertTrue(board_positions.count('rook') == 2) + self.assertTrue(board_positions.count('pawn') == 18) + self.assertTrue(board_positions.count('knight') == 4) + self.assertTrue(board_positions.count('king') == 2) + self.assertTrue(board_positions.count('gold') == 4) + self.assertTrue(board_positions.count('silver') == 4) + self.assertTrue(board_positions.count('lance') == 4) + + def test_complex_position(self): + board = shogi.Board('4+R3l/1r1+P2gk1/3p1p1s1/2pg3pp/1p4p2/SP4PPP/2NP1PKS1/2G2+n3/8L w B2N2L3Pbgsp 10') + svg_file = board.svg() + pieces = svg_file.split('')[1] + board_positions = pieces.split('')[0] + hand_positions = pieces.split('')[1] + + # check board pieces + self.assertTrue(board_positions.count('bishop') == 0) + self.assertTrue(board_positions.count('rook') == 1) + self.assertTrue(board_positions.count('pawn') == 14) + self.assertTrue(board_positions.count('knight') == 2) + self.assertTrue(board_positions.count('king') == 2) + self.assertTrue(board_positions.count('gold') == 3) + self.assertTrue(board_positions.count('silver') == 3) + self.assertTrue(board_positions.count('lance') == 2) + self.assertTrue(board_positions.count('dragon') == 1) + self.assertTrue(board_positions.count('pro-pawn') == 1) + self.assertTrue(board_positions.count('pro-knight') == 1) + + # check hand pieces (only checking 1 count per player) + self.assertTrue(hand_positions.count('bishop') == 2) + self.assertTrue(hand_positions.count('gold') == 1) + self.assertTrue(hand_positions.count('silver') == 1) + self.assertTrue(hand_positions.count('pawn') == 2) + self.assertTrue(hand_positions.count('knight') == 1) + self.assertTrue(hand_positions.count('lance') == 1) + + def test_two_digit_in_hand(self): + # 11 pawns in sente's hand + board = shogi.Board('4+R3l/1r1+P2gk1/3p3s1/2pg5/1p4p2/SP7/2N2PKS1/2G2+n3/8L b b2n2lBGS11P 50') + svg_file = board.svg() + pieces = svg_file.split('')[1] + board_positions = pieces.split('')[0] + hand_positions = pieces.split('')[1] + + # check board pieces + self.assertTrue(board_positions.count('bishop') == 0) + self.assertTrue(board_positions.count('rook') == 1) + self.assertTrue(board_positions.count('pawn') == 7) + self.assertTrue(board_positions.count('knight') == 2) + self.assertTrue(board_positions.count('king') == 2) + self.assertTrue(board_positions.count('gold') == 3) + self.assertTrue(board_positions.count('silver') == 3) + self.assertTrue(board_positions.count('lance') == 2) + self.assertTrue(board_positions.count('dragon') == 1) + self.assertTrue(board_positions.count('pro-pawn') == 1) + self.assertTrue(board_positions.count('pro-knight') == 1) + + # check hand pieces (only checking 1 count per player) + self.assertTrue(hand_positions.count('bishop') == 2) + self.assertTrue(hand_positions.count('gold') == 1) + self.assertTrue(hand_positions.count('silver') == 1) + self.assertTrue(hand_positions.count('pawn') == 1) + self.assertTrue(hand_positions.count('knight') == 1) + self.assertTrue(hand_positions.count('lance') == 1) + self.assertTrue(hand_positions.count('>11<') == 1)