├── .gitignore
├── MANIFEST.in
├── README.rst
├── README.sdist.txt
├── build_docs
├── doc
├── _static
│ └── sgfmill.css_t
├── _templates
│ └── wholetoc.html
├── ascii_boards.rst
├── boards.rst
├── changes.rst
├── common.rst
├── conf.py
├── contact.rst
├── encoding.rst
├── examples.rst
├── glossary.rst
├── index.rst
├── install.rst
├── intro.rst
├── licence.rst
├── parsing.rst
├── porting.rst
├── properties.rst
├── property_types.rst
├── python-inv.txt
├── sgf.rst
├── sgf_board_interface.rst
├── sgf_games.rst
├── sgf_moves.rst
├── sgfmill_package.rst
└── tree_nodes.rst
├── examples
├── show_sgf.py
└── split_sgf_collection.py
├── release_sgfmill.py
├── run_sgfmill_testsuite
├── setup.py
├── sgfmill
├── __init__.py
├── ascii_boards.py
├── boards.py
├── common.py
├── sgf.py
├── sgf_board_interface.py
├── sgf_grammar.py
├── sgf_moves.py
└── sgf_properties.py
├── sgfmill_tests
├── __init__.py
├── board_test_data.py
├── board_tests.py
├── common_tests.py
├── run_sgfmill_testsuite.py
├── sgf_grammar_tests.py
├── sgf_moves_tests.py
├── sgf_properties_tests.py
├── sgf_tests.py
├── sgfmill_test_support.py
└── test_framework.py
└── test_installed_sgfmill.py
/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__
2 | /doc/_build
3 | /release_sgfmill.conf
4 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | graft examples
2 | prune examples/__pycache__
3 | graft sgfmill_tests
4 | prune sgfmill_tests/__pycache__
5 | graft doc
6 | prune doc/_build
7 | include build_docs
8 | include run_sgfmill_testsuite
9 | include test_installed_sgfmill.py
10 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | =======
2 | Sgfmill
3 | =======
4 |
5 | Sgfmill is a Python library for reading and writing Go game records using
6 | Smart Game Format (SGF).
7 |
8 | Full documentation and contact information is available from the `home page`__.
9 |
10 | .. __: http://mjw.woodcraft.me.uk/sgfmill/
11 |
12 |
13 | Requirements
14 | ------------
15 |
16 | Sgfmill requires Python 3.2 or later. There are no other requirements.
17 |
18 | This is a Python 3 version of the SGF code from the Python 2 Gomill__ project.
19 | If you need Python 2 support, please use Gomill instead.
20 |
21 | .. __: https://mjw.woodcraft.me.uk/gomill/
22 |
23 |
24 | Building the documentation
25 | --------------------------
26 |
27 | To build the HTML documentation yourself::
28 |
29 | ./build_docs
30 |
31 | The documentation will be generated in ``doc/_build/html``.
32 |
33 | Requirements:
34 |
35 | - Sphinx__ version 1.0 or later (tested with 1.4)
36 |
37 | .. __: http://sphinx.pocoo.org/
38 |
39 |
--------------------------------------------------------------------------------
/README.sdist.txt:
--------------------------------------------------------------------------------
1 | Sgfmill
2 | =======
3 |
4 | Sgfmill is a Python library for reading and writing Go game records using
5 | Smart Game Format (SGF).
6 |
7 | Sgfmill's home page is http://mjw.woodcraft.me.uk/sgfmill/
8 |
9 | There is also a Github repository at https://github.com/mattheww/sgfmill
10 |
11 |
12 | Requirements
13 | ------------
14 |
15 | Sgfmill requires Python 3.2 or later. There are no other requirements.
16 |
17 | This is a Python 3 version of the SGF code from the Python 2 Gomill project
18 | . If you need Python 2 support, please
19 | use Gomill instead.
20 |
21 |
22 | Installation
23 | ------------
24 |
25 | Installing Sgfmill puts the sgfmill package onto the Python module search path.
26 |
27 | To install from the source distribution:
28 |
29 | python3 setup.py bdist_wheel
30 | pip3 install --user dist/sgfmill-*.whl
31 |
32 | To uninstall:
33 |
34 | pip3 uninstall sgfmill
35 |
36 |
37 | Running the test suite
38 | ----------------------
39 |
40 | To run the testsuite against the distributed sgfmill package, change to
41 | the distribution directory and run
42 |
43 | ./run_sgfmill_testsuite
44 |
45 |
46 | To run the testsuite against an installed sgfmill package, change to the
47 | distribution directory and run
48 |
49 | python3 test_installed_sgfmill.py
50 |
51 |
52 | Running the example scripts
53 | ---------------------------
54 |
55 | To run the example scripts, it is simplest to install the sgfmill package
56 | first.
57 |
58 | If you do not wish to do so, you can run
59 |
60 | export PYTHONPATH=
61 |
62 | so that the example scripts will be able to find the sgfmill package.
63 |
64 |
65 | Building the documentation
66 | --------------------------
67 |
68 | To build the documentation, change to the distribution directory and run
69 |
70 | ./build_docs
71 |
72 | The documentation will be generated in doc/_build/html.
73 |
74 | Requirements:
75 |
76 | Sphinx [1] version 1.0 or later (tested with 1.4)
77 |
78 | [1] http://sphinx.pocoo.org/
79 |
80 |
81 | Licence
82 | -------
83 |
84 | Sgfmill is copyright 2009-2018 Matthew Woodcraft and the sgfmill contributors
85 |
86 | Permission is hereby granted, free of charge, to any person obtaining a copy
87 | of this software and associated documentation files (the "Software"), to deal
88 | in the Software without restriction, including without limitation the rights
89 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
90 | copies of the Software, and to permit persons to whom the Software is
91 | furnished to do so, subject to the following conditions:
92 |
93 | The above copyright notice and this permission notice shall be included in all
94 | copies or substantial portions of the Software.
95 |
96 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
97 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
98 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
99 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
100 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
101 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
102 | SOFTWARE.
103 |
104 |
105 | Contributors
106 | ------------
107 |
108 | See the 'Licence' page in the HTML documentation (doc/licence.rst).
109 |
110 |
111 | Contact
112 | -------
113 |
114 | Please send any bug reports, suggestions, patches, questions &c to
115 |
116 | Matthew Woodcraft
117 | matthew@woodcraft.me.uk
118 |
119 |
120 | Changelog
121 | ---------
122 |
123 | See the 'Changes' page in the HTML documentation (doc/changes.rst).
124 |
125 | mjw 2018-05-20
126 |
--------------------------------------------------------------------------------
/build_docs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import argparse
4 | import os
5 | import shutil
6 | import sys
7 |
8 | from pathlib import Path
9 |
10 | import sphinx
11 |
12 | def main():
13 | parser = argparse.ArgumentParser()
14 | parser.add_argument("--all", "-a", action="store_true")
15 | parser.add_argument("--nitpick", "-n", action="store_true")
16 | args = parser.parse_args()
17 |
18 | project_dir = Path(Path.cwd(), __file__).parent
19 | sys.path.append(str(project_dir))
20 |
21 | docs_dir = Path(project_dir, "doc")
22 | build_dir = Path(docs_dir, "_build")
23 |
24 | if args.all:
25 | if build_dir.exists():
26 | shutil.rmtree(str(build_dir))
27 |
28 | argv = ["build_docs"]
29 | if args.nitpick:
30 | argv.append("-n")
31 | argv += [
32 | "-b", "html",
33 | "-d", str(Path(build_dir, "doctrees")),
34 | str(docs_dir),
35 | str(Path(build_dir, "html"))
36 | ]
37 | sys.exit(sphinx.main(argv))
38 |
39 | main()
40 |
--------------------------------------------------------------------------------
/doc/_static/sgfmill.css_t:
--------------------------------------------------------------------------------
1 | @import url("default.css");
2 |
3 | p.topic-title {
4 | color: {{ theme_headtextcolor }};
5 | }
6 |
7 | li.current > a {color: yellow;}
8 |
9 | div.tip {
10 | background-color: #EEEEEE;
11 | border: 1px solid #CCCCCC;
12 | }
13 |
14 | div.caution {
15 | background-color: #EEEEEE;
16 | border: 1px solid #A00000;
17 | }
18 |
19 | abbr {
20 | border-bottom: none;
21 | text-decoration: none;
22 | }
23 |
24 | th.field-name {
25 | background-color: #E4E4E4;
26 | font-weight: normal;
27 | }
28 |
29 | div#modules table.docutils {
30 | width : 100%;
31 | margin-bottom: 3ex;
32 | }
33 |
34 |
--------------------------------------------------------------------------------
/doc/_templates/wholetoc.html:
--------------------------------------------------------------------------------
1 |
2 | {{ toctree(collapse=False) }}
3 |
--------------------------------------------------------------------------------
/doc/ascii_boards.rst:
--------------------------------------------------------------------------------
1 | The :mod:`!ascii_boards` module
2 | -------------------------------
3 |
4 | .. module:: sgfmill.ascii_boards
5 | :synopsis: ASCII Go board diagrams.
6 |
7 | The :mod:`!sgfmill.ascii_boards` module contains functions for producing and
8 | interpreting ASCII diagrams of Go board positions.
9 |
10 | It supports square boards up to size 25x25, which is the upper limit of the
11 | notation it uses (and also the upper limit for |gtp|).
12 |
13 |
14 | .. function:: render_board(board)
15 |
16 | :rtype: string
17 |
18 | Returns an ASCII diagram of the position on the :class:`.Board` *board*.
19 |
20 | The returned string does not end with a newline.
21 |
22 | ::
23 |
24 | >>> b = boards.Board(9)
25 | >>> b.play(2, 5, 'b')
26 | >>> b.play(3, 6, 'w')
27 | >>> print(ascii_boards.render_board(b))
28 | 9 . . . . . . . . .
29 | 8 . . . . . . . . .
30 | 7 . . . . . . . . .
31 | 6 . . . . . . . . .
32 | 5 . . . . . . . . .
33 | 4 . . . . . . o . .
34 | 3 . . . . . # . . .
35 | 2 . . . . . . . . .
36 | 1 . . . . . . . . .
37 | A B C D E F G H J
38 |
39 | See also the :script:`show_sgf.py` example script.
40 |
41 |
42 | .. function:: interpret_diagram(diagram, size[, board])
43 |
44 | :rtype: :class:`.Board`
45 |
46 | Returns the position given in an ASCII diagram.
47 |
48 | *diagram* should be a string in the format returned by
49 | :func:`render_board`, representing a position with the specified size.
50 | Leading and trailing whitespace is ignored.
51 |
52 | If the diagram is not in the right form, this function may raise
53 | :exc:`ValueError` or may return a 'best guess'.
54 |
55 | If the optional *board* parameter is provided, it must be an empty
56 | :class:`.Board` of the right size; the same object will be returned (this
57 | option is provided so you can use a different Board class).
58 |
--------------------------------------------------------------------------------
/doc/boards.rst:
--------------------------------------------------------------------------------
1 | The :mod:`!boards` module
2 | -------------------------
3 |
4 | .. module:: sgfmill.boards
5 | :synopsis: Go board representation.
6 |
7 | The :mod:`!sgfmill.boards` module provides a Go board representation for use
8 | with the functions in :mod:`sgfmill.sgf_moves`.
9 |
10 | Everything in this module works with boards of arbitrarily large sizes.
11 |
12 | The implementation is not designed for speed (even as Python code goes), and
13 | is certainly not appropriate for implementing a playing engine.
14 |
15 | The module contains a single class:
16 |
17 |
18 | .. class:: Board(side)
19 |
20 | A :class:`!Board` object represents a legal position on a Go board.
21 |
22 | Instantiate with the board size, as an int >= 1. Only square boards are
23 | supported. The board is initially empty.
24 |
25 | Board objects do not maintain any history information.
26 |
27 | Board objects have the following attributes (which should be treated as
28 | read-only):
29 |
30 | .. attribute:: side
31 |
32 | The board size.
33 |
34 | .. attribute:: board_points
35 |
36 | A list of *points*, giving all points on the board.
37 |
38 |
39 | The principal :class:`!Board` methods are :meth:`!get` and :meth:`!play`.
40 | Their *row* and *col* parameters should be ints representing coordinates in
41 | the :ref:`system ` used for a *point*.
42 |
43 | .. method:: Board.get(row, col)
44 |
45 | :rtype: *colour* or ``None``
46 |
47 | Returns the contents of the specified point.
48 |
49 | Raises :exc:`IndexError` if the coordinates are out of range.
50 |
51 | .. method:: Board.play(row, col, colour)
52 |
53 | :rtype: *move*
54 |
55 | Places a stone of the specified *colour* on the specified point.
56 |
57 | Raises :exc:`IndexError` if the coordinates are out of range.
58 |
59 | Raises :exc:`ValueError` if the point isn't empty.
60 |
61 | Carries out any captures which follow from the placement, including
62 | self-captures.
63 |
64 | This method doesn't enforce any ko rule.
65 |
66 | The return value indicates whether, immediately following this move, any
67 | point would be forbidden by the :term:`simple ko` rule. If so, that point
68 | is returned; otherwise the return value is ``None``.
69 |
70 |
71 | The other :class:`!Board` methods are:
72 |
73 | .. method:: Board.is_empty()
74 |
75 | :rtype: bool
76 |
77 | Returns ``True`` if all points on the board are empty.
78 |
79 | .. method:: Board.list_occupied_points()
80 |
81 | :rtype: list of pairs (*colour*, *point*)
82 |
83 | Returns a list of all nonempty points, in unspecified order.
84 |
85 | .. method:: Board.area_score()
86 |
87 | :rtype: int
88 |
89 | Calculates the area score of a position, assuming that all stones are
90 | alive. The result is the number of points controlled (occupied or
91 | surrounded) by Black minus the number of points controlled by White.
92 |
93 | Doesn't take any :term:`komi` into account.
94 |
95 | .. method:: Board.copy()
96 |
97 | :rtype: :class:`!Board`
98 |
99 | Returns an independent copy of the board.
100 |
101 | .. method:: Board.apply_setup(black_points, white_points, empty_points)
102 |
103 | :rtype: bool
104 |
105 | Adds and/or removes stones on arbitrary points. This is intended to support
106 | behaviour like |sgf| ``AB``/``AW``/``AE`` properties.
107 |
108 | Each parameter is an iterable of *points*.
109 |
110 | Raises :exc:`IndexError` if any points are out of range.
111 |
112 | This method applies all the specified additions and removals, then removes
113 | any groups with no liberties (so the resulting position is always legal).
114 |
115 | If the same point is specified in more than one list, the order in which
116 | the instructions are applied is undefined.
117 |
118 | Returns ``True`` if the position was legal as specified.
119 |
--------------------------------------------------------------------------------
/doc/changes.rst:
--------------------------------------------------------------------------------
1 | Changes
2 | =======
3 |
4 | * Sped up the :class:`.Board` implementation (thanks to Lin, Yong Xiang).
5 |
6 | * The minimum Python version is now 3.5.
7 |
8 |
9 | Sgfmill 1.1.1 (2018-05-20)
10 | --------------------------
11 |
12 | * Sped up the :class:`.Board` implementation (thanks to Seth Troisi).
13 |
14 |
15 | Sgfmill 1.1 (2018-02-11)
16 | ------------------------
17 |
18 | * The parser now permits lower-case letters in *PropIdents*; see
19 | :doc:`parsing` for details.
20 |
21 | * Bug fix: :meth:`.Tree_node.set` didn't check its ``identifier`` parameter
22 | was a well-formed *PropIdent*.
23 |
24 | * Bug fix: :meth:`.Tree_node.set` was willing to store invalid values for
25 | properties of type Number, if fed invalid input.
26 |
27 |
28 | Sgfmill 1.0 (2017-04-17)
29 | ------------------------
30 |
31 | * Python 3 port of the SGF code from Gomill__ 0.8.
32 |
33 | * Added the :mod:`.sgf_board_interface` module.
34 |
35 | .. __: https://mjw.woodcraft.me.uk/gomill/
36 |
37 |
--------------------------------------------------------------------------------
/doc/common.rst:
--------------------------------------------------------------------------------
1 | The :mod:`~sgfmill.common` module
2 | ---------------------------------
3 |
4 | .. module:: sgfmill.common
5 | :synopsis: Go-related utility functions.
6 |
7 | The :mod:`!sgfmill.common` module provides a few Go-related utility functions,
8 | mostly used only by :mod:`sgfmill.ascii_boards`.
9 |
10 | It is designed to be safe to use as ``from common import *``.
11 |
12 | The vertex functions suppose square boards up to size 25x25, which is the
13 | upper limit of the notation they use (and also the upper limit for |gtp|).
14 |
15 | .. function:: opponent_of(colour)
16 |
17 | :rtype: *colour*
18 |
19 | Returns the other colour::
20 |
21 | >>> opponent_of('b')
22 | 'w'
23 |
24 | .. function:: colour_name(colour)
25 |
26 | :rtype: string
27 |
28 | Returns the (lower-case) full name of a *colour*::
29 |
30 | >>> colour_name('b')
31 | 'black'
32 |
33 | .. function:: format_vertex(move)
34 |
35 | :rtype: string
36 |
37 | Returns a string describing a *move* in conventional notation::
38 |
39 | >>> format_vertex((3, 0))
40 | 'A4'
41 | >>> format_vertex(None)
42 | 'pass'
43 |
44 | The result is suitable for use directly in |GTP| responses. Note that ``I``
45 | is omitted from the letters used to indicate columns, so the maximum
46 | supported column value is ``25``.
47 |
48 | .. function:: format_vertex_list(moves)
49 |
50 | :rtype: string
51 |
52 | Returns a string describing a sequence of *moves*::
53 |
54 | >>> format_vertex_list([(0, 1), (2, 3), None])
55 | 'B1,D3,pass'
56 | >>> format_vertex_list([])
57 | ''
58 |
59 | .. function:: move_from_vertex(vertex, board_size)
60 |
61 | :rtype: *move*
62 |
63 | Interprets the string *vertex* as conventional notation, assuming a square
64 | board whose side is *board_size*::
65 |
66 | >>> move_from_vertex("A4", 9)
67 | (3, 0)
68 | >>> move_from_vertex("a4", 9)
69 | (3, 0)
70 | >>> move_from_vertex("pass", 9)
71 | None
72 |
73 | Raises :exc:`ValueError` if it can't parse the string, or if the resulting
74 | point would be off the board.
75 |
76 | Treats *vertex* case-insensitively.
77 |
78 |
--------------------------------------------------------------------------------
/doc/conf.py:
--------------------------------------------------------------------------------
1 | import sgfmill
2 |
3 | extensions = [
4 | 'sphinx.ext.intersphinx',
5 | 'sphinx.ext.viewcode',
6 | ]
7 | templates_path = ['_templates']
8 | source_suffix = '.rst'
9 | source_encoding = 'utf-8'
10 | master_doc = 'index'
11 | project = u'Sgfmill'
12 | copyright = u'2009-2018, Matthew Woodcraft and the sgfmill contributors'
13 | version = sgfmill.__version__
14 | release = sgfmill.__version__
15 | exclude_patterns = ['_build']
16 | pygments_style = 'vs'
17 | modindex_common_prefix = ['sgfmill.']
18 |
19 | html_theme = 'default'
20 | html_theme_options = {
21 | 'nosidebar' : False,
22 | #'rightsidebar' : True,
23 | 'stickysidebar' : False,
24 |
25 | 'footerbgcolor' : '#3d3011',
26 | #'footertextcolor' : ,
27 | 'sidebarbgcolor' : '#3d3011',
28 | #'sidebartextcolor' : ,
29 | 'sidebarlinkcolor' : '#d8d898',
30 | 'relbarbgcolor' : '#523f13',
31 | #'relbartextcolor' : ,
32 | #'relbarlinkcolor' : ,
33 | #'bgcolor' : ,
34 | #'textcolor' : ,
35 | 'linkcolor' : '#7c5f35',
36 | 'visitedlinkcolor' : '#7c5f35',
37 | #'headbgcolor' : ,
38 | 'headtextcolor' : '#5c4320',
39 | #'headlinkcolor' : ,
40 | #'codebgcolor' : ,
41 | #'codetextcolor' : ,
42 |
43 | 'externalrefs' : True,
44 | }
45 |
46 | html_static_path = ['_static']
47 | html_sidebars = { '**':[
48 | 'wholetoc.html',
49 | 'searchbox.html',
50 | ], }
51 | html_style = "sgfmill.css"
52 |
53 | intersphinx_mapping = {'python': ('http://docs.python.org/3',
54 | 'python-inv.txt')}
55 |
56 | rst_epilog = """
57 | .. |gtp| replace:: :abbr:`GTP (Go Text Protocol)`
58 | .. |sgf| replace:: :abbr:`SGF (Smart Game Format)`
59 | """
60 |
61 | def setup(app):
62 | app.add_object_type('script', 'script',
63 | indextemplate='pair: %s; example script',
64 | objname="Example script")
65 |
66 |
--------------------------------------------------------------------------------
/doc/contact.rst:
--------------------------------------------------------------------------------
1 | Contact
2 | =======
3 |
4 | Sgfmill's home page is http://mjw.woodcraft.me.uk/sgfmill/.
5 |
6 | Updated versions will be made available for download from that site, as well
7 | as the `Python package index`__.
8 |
9 | .. __: https://pypi.python.org/pypi/sgfmill/
10 |
11 | I'm happy to receive any bug reports, suggestions, patches, questions and so
12 | on at .
13 |
14 | There is also a Github repository at https://github.com/mattheww/sgfmill.
15 |
--------------------------------------------------------------------------------
/doc/encoding.rst:
--------------------------------------------------------------------------------
1 | Character encoding
2 | ==================
3 |
4 | .. currentmodule:: sgfmill.sgf
5 |
6 | The |sgf| format is defined as containing ASCII-encoded data, possibly with
7 | non-ASCII characters in Text and SimpleText property values. The low-level
8 | Sgfmill functions for loading and serialising |sgf| data work with Python
9 | bytes or bytes-like objects.
10 |
11 | The encoding used for Text and SimpleText property values is given by the
12 | ``CA`` root property (if that isn't present, the encoding is ``ISO-8859-1``).
13 |
14 | In order for an encoding to be used in Sgfmill, it must exist as a Python
15 | built-in codec, and it must be compatible with ASCII (at least whitespace,
16 | ``\``, ``]``, and ``:`` must be in the usual places). Behaviour is unspecified
17 | if a non-ASCII-compatible encoding is requested.
18 |
19 | When encodings are passed as parameters (or returned from functions), they are
20 | represented using the names or aliases of Python built-in codecs (eg
21 | ``"UTF-8"`` or ``"ISO-8859-1"``). See `standard encodings`__ for a list.
22 | Values of the ``CA`` property are interpreted in the same way.
23 |
24 | .. __: https://docs.python.org/3/library/codecs.html#standard-encodings
25 |
26 |
27 | .. _raw_property_encoding:
28 |
29 | The raw property encoding
30 | -------------------------
31 |
32 | Each :class:`.Sgf_game` and :class:`.Tree_node` has a fixed :dfn:`raw property
33 | encoding`, which is the encoding used internally to store the property values.
34 | The :meth:`Tree_node.get_raw` and :meth:`Tree_node.set_raw` methods use the
35 | raw property encoding.
36 |
37 | When an |sgf| game is loaded from a bytes-like object, the raw property
38 | encoding is taken from the ``CA`` root property (unless overridden).
39 | Improperly encoded property values will not be detected until they are
40 | accessed (:meth:`~Tree_node.get` will raise :exc:`ValueError`; use
41 | :meth:`~Tree_node.get_raw` to retrieve the actual bytes).
42 |
43 | When an |sgf| game is created from a Python string (which contains Unicode
44 | characters), the raw property encoding is always ``UTF-8``.
45 |
46 |
47 | .. _changing_ca:
48 |
49 | Changing the CA property
50 | ------------------------
51 |
52 | When an |sgf| game is serialised to a string, the encoding represented by the
53 | ``CA`` root property is used. This :dfn:`target encoding` will be the same as
54 | the raw property encoding unless ``CA`` has been changed since the
55 | :class:`.Sgf_game` was created.
56 |
57 | When the raw property encoding and the target encoding match, the raw property
58 | values are included unchanged in the output (even if they are improperly
59 | encoded.)
60 |
61 | Otherwise, if any raw property value is improperly encoded,
62 | :exc:`UnicodeDecodeError` is raised, and if any property value can't be
63 | represented in the target encoding, :exc:`UnicodeEncodeError` is raised.
64 |
65 | If the target encoding doesn't identify a Python codec, :exc:`ValueError` is
66 | raised. The behaviour of :meth:`~Sgf_game.serialise` is unspecified if the
67 | target encoding isn't ASCII-compatible (eg, UTF-16).
68 |
69 |
70 | .. _transcoding:
71 |
72 | Transcoding
73 | -----------
74 |
75 | Because changing the ``CA`` property has no effect until you serialise the
76 | game, it doesn't broaden the set of characters you can use when you
77 | :meth:`~Tree_node.set` a property.
78 |
79 | If you plan to save a file as ``UTF-8`` and want to be able to set arbitrary
80 | strings, you can ensure the raw property encoding is ``UTF-8`` by changing
81 | ``CA`` and reloading the game::
82 |
83 | game = sgf.Sgf_game.from_bytes(...)
84 | game.get_root().set("CA", "utf-8")
85 | game = sgf.Sgf_game.from_bytes(game.serialise())
86 | game.get_root().set("PB", "本因坊秀策")
87 |
88 |
--------------------------------------------------------------------------------
/doc/examples.rst:
--------------------------------------------------------------------------------
1 | Examples
2 | ========
3 |
4 | Reading from a file
5 | -------------------
6 |
7 | ::
8 |
9 | from sgfmill import sgf
10 | with open("foo.sgf", "rb") as f:
11 | game = sgf.Sgf_game.from_bytes(f.read())
12 | winner = game.get_winner()
13 | board_size = game.get_size()
14 | root_node = game.get_root()
15 | b_player = root_node.get("PB")
16 | w_player = root_node.get("PW")
17 | for node in game.get_main_sequence():
18 | print(node.get_move())
19 |
20 |
21 | Recording a game
22 | ----------------
23 |
24 | ::
25 |
26 | from sgfmill import sgf
27 | game = sgf.Sgf_game(size=13)
28 | for move_info in ...:
29 | node = game.extend_main_sequence()
30 | node.set_move(move_info.colour, move_info.move)
31 | if move_info.comment is not None:
32 | node.set("C", move_info.comment)
33 | with open(pathname, "wb") as f:
34 | f.write(game.serialise())
35 |
36 |
37 | Modifying a game
38 | ----------------
39 |
40 | ::
41 |
42 | >>> from sgfmill import sgf
43 | >>> game = sgf.Sgf_game.from_string("(;FF[4]GM[1]SZ[9];B[ee];W[ge])")
44 | >>> root_node = game.get_root()
45 | >>> root_node.set("RE", "B+R")
46 | >>> new_node = game.extend_main_sequence()
47 | >>> new_node.set_move("b", (2, 3))
48 | >>> [node.get_move() for node in game.get_main_sequence()]
49 | [(None, None), ('b', (4, 4)), ('w', (4, 6)), ('b', (2, 3))]
50 | >>> game.serialise()
51 | b'(;FF[4]CA[UTF-8]GM[1]RE[B+R]SZ[9];B[ee];W[ge];B[dg])\n'
52 |
53 |
54 | See also the :script:`show_sgf.py` and :script:`split_sgf_collection.py`
55 | example scripts.
56 |
57 |
58 | The example scripts
59 | -------------------
60 |
61 | The following example scripts are available in the :file:`examples/` directory
62 | of the Sgfmill source distribution.
63 |
64 | They may be independently useful, as well as illustrating the library API.
65 |
66 | See the top of each script for further information.
67 |
68 | See :ref:`running the example scripts ` for notes
69 | on making the :mod:`!sgfmill` package available for use with the example
70 | scripts.
71 |
72 |
73 | .. script:: show_sgf.py
74 |
75 | Prints an ASCII diagram of the position from an |sgf| file.
76 |
77 | This demonstrates the :mod:`~sgfmill.sgf`, :mod:`~sgfmill.sgf_moves`, and
78 | :mod:`~sgfmill.ascii_boards` modules.
79 |
80 |
81 | .. script:: split_sgf_collection.py
82 |
83 | Splits a file containing an |sgf| game collection into multiple files.
84 |
85 | This demonstrates the parsing functions from the :mod:`!sgf_grammar` module.
86 |
87 |
88 |
--------------------------------------------------------------------------------
/doc/glossary.rst:
--------------------------------------------------------------------------------
1 | Glossary
2 | ========
3 |
4 | .. glossary::
5 |
6 | GTP
7 | The Go Text Protocol
8 |
9 | A communication protocol used to control Go-playing programs. Gomill
10 | uses only GTP version 2, which is specified at
11 | http://www.lysator.liu.se/~gunnar/gtp/gtp2-spec-draft2/gtp2-spec.html.
12 |
13 | (As of April 2017, the specification describes itself as a draft, but it
14 | has remained stable for several years and is widely implemented.)
15 |
16 |
17 | SGF
18 | The Smart Game Format
19 |
20 | A text-based file format used for storing Go game records.
21 |
22 | Gomill uses version FF[4], which is specified at
23 | http://www.red-bean.com/sgf/index.html.
24 |
25 |
26 | komi
27 | Additional points awarded to White in final scoring.
28 |
29 |
30 | simple ko
31 | A Go rule prohibiting repetition of the immediately-preceding position.
32 |
33 |
34 |
--------------------------------------------------------------------------------
/doc/index.rst:
--------------------------------------------------------------------------------
1 | *******
2 | Sgfmill
3 | *******
4 |
5 | .. toctree::
6 | :maxdepth: 3
7 | :titlesonly:
8 | :includehidden:
9 |
10 | intro
11 | examples
12 | property_types
13 | sgfmill_package
14 | properties
15 | encoding
16 | parsing
17 | porting
18 | install
19 | contact
20 | changes
21 | licence
22 | glossary
23 |
24 | * :ref:`genindex`
25 | * :ref:`search`
26 |
27 |
--------------------------------------------------------------------------------
/doc/install.rst:
--------------------------------------------------------------------------------
1 | Installation
2 | ============
3 |
4 | .. contents:: Page contents
5 | :local:
6 | :backlinks: none
7 |
8 |
9 | Requirements
10 | ------------
11 |
12 | Sgfmill requires Python 3.2 or later. There are no other requirements.
13 |
14 | This is a Python 3 version of the |sgf| code from the Python 2 Gomill__ project.
15 | If you need Python 2 support, please use Gomill instead.
16 |
17 | .. __: https://mjw.woodcraft.me.uk/gomill/
18 |
19 |
20 | Installing
21 | ----------
22 |
23 | Sgfmill can be installed from the Python Package Index::
24 |
25 | pip3 install sgfmill
26 |
27 | To remove an installed version of Sgfmill, run ::
28 |
29 | pip3 uninstall sgfmill
30 |
31 |
32 | Downloading sources and documentation
33 | -------------------------------------
34 |
35 | The source distribution can be downloaded from the `Python Package index`__,
36 | or from http://mjw.woodcraft.me.uk/sgfmill/, as a file named
37 | :file:`sgfmill-{version}.tar.gz`.
38 |
39 | .. __: https://pypi.python.org/pypi/sgfmill
40 |
41 | This documentation is distributed separately in html form at
42 | http://mjw.woodcraft.me.uk/sgfmill/ as :file:`sgfmill-doc-{version}.tar.gz`.
43 |
44 | The version-control history is available at
45 | https://github.com/mattheww/sgfmill.
46 |
47 | To install from the source distribution::
48 |
49 | python3 setup.py bdist_wheel
50 | pip3 install --user dist/sgfmill-*.whl
51 |
52 |
53 | Running the test suite
54 | ----------------------
55 |
56 | The testsuite is available from the source distribution or a version-control
57 | checkout.
58 |
59 | To run the testsuite against the distributed :mod:`!sgfmill` package, change to
60 | the distribution directory and run ::
61 |
62 | ./run_sgfmill_testsuite
63 |
64 |
65 | To run the testsuite against an installed :mod:`!sgfmill` package, change to
66 | the distribution directory and run ::
67 |
68 | python test_installed_sgfmill.py
69 |
70 |
71 | .. _running the example scripts:
72 |
73 | Running the example scripts
74 | ---------------------------
75 |
76 | The example scripts are included in the source distribution. To run them, it
77 | is simplest to install the :mod:`!sgfmill` package first.
78 |
79 | If you do not wish to do so, you can run ::
80 |
81 | export PYTHONPATH=
82 |
83 | so that the example scripts will be able to find the :mod:`!sgfmill` package.
84 |
85 |
86 |
87 | Building the documentation
88 | --------------------------
89 |
90 | The sources for this HTML documentation are included in the Sgfmill source
91 | distribution. To build the documentation, change to the distribution directory
92 | and run ::
93 |
94 | ./build_docs
95 |
96 | The documentation will be generated in :file:`doc/_build/html`.
97 |
98 | Requirements:
99 |
100 | - Sphinx__ version 1.0 or later (tested with 1.2)
101 |
102 | .. __: http://sphinx.pocoo.org/
103 |
104 |
--------------------------------------------------------------------------------
/doc/intro.rst:
--------------------------------------------------------------------------------
1 | Introduction
2 | ============
3 |
4 | Sgfmill is a Python library for reading and writing Go game records using
5 | Smart Game Format (:term:`SGF`).
6 |
7 | It supports:
8 |
9 | * loading |sgf| game records to make a Python object representation
10 | * creating |sgf| game objects from scratch
11 | * setting properties and manipulating the tree structure
12 | * serialising game records to |sgf| data
13 | * applying setup stones and moves to a Go board position
14 |
15 | It is intended for use with |SGF| version FF[4], which is specified at
16 | http://www.red-bean.com/sgf/index.html.
17 |
18 | It has support for the game-specific properties for Go, but not those of other
19 | games.
20 |
21 | Point, Move and Stone values are interpreted as Go points.
22 |
23 |
24 | Python language support
25 | -----------------------
26 |
27 | Sgfmill requires Python 3.5 or newer.
28 |
29 | It is a Python 3 version of the |sgf| code from the Python 2 Gomill__ project.
30 | If you need Python 2 support, please use Gomill instead.
31 |
32 | If you need support for older versions of Python 3, you can use sgfmill 1.1.1.
33 |
34 | .. __: https://mjw.woodcraft.me.uk/gomill/
35 |
36 |
--------------------------------------------------------------------------------
/doc/licence.rst:
--------------------------------------------------------------------------------
1 | Licence
2 | =======
3 |
4 | Sgfmill is copyright 2009-2018 Matthew Woodcraft and the sgfmill contributors
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 |
24 | .. Note:: This is the licence commonly known as the 'MIT' Licence.
25 |
26 |
27 | Contributors
28 | ------------
29 |
30 | * Matthew Woodcraft
31 | * Seth Troisi
32 |
33 |
--------------------------------------------------------------------------------
/doc/parsing.rst:
--------------------------------------------------------------------------------
1 | Parser behaviour
2 | ================
3 |
4 | The parser permits non-|sgf| content to appear before the beginning and after
5 | the end of the game. It identifies the start of |sgf| content by looking for
6 | ``(;`` (with possible whitespace between the two characters).
7 |
8 | The parser accepts at most 64 letters in *PropIdents* (there is no formal limit
9 | in the specification, but no standard property has more than 2; strings as
10 | long as 9 letters have been found in the wild).
11 |
12 | The parser doesn't perform any checks on property values. In particular, it
13 | allows multiple values to be present for any property.
14 |
15 | The parser doesn't, in general, attempt to 'fix' ill-formed |sgf| content. As
16 | an exception, if a *PropIdent* appears more than once in a node it is
17 | converted to a single property with multiple values.
18 |
19 | The parser permits lower-case letters in *PropIdents* (these are allowed in
20 | some ancient |sgf| variants, and are apparently seen in the wild). It ignores
21 | those letters, so for example ``CoPyright`` is treated as a synonym for ``CP``
22 | and should be retrieved using ``node.get("CP")``.
23 |
24 |
--------------------------------------------------------------------------------
/doc/porting.rst:
--------------------------------------------------------------------------------
1 | Porting to Python 3
2 | ===================
3 |
4 | .. currentmodule:: sgfmill.sgf
5 |
6 | Sgfmill is a Python 3 version of the |sgf| code from the Gomill__ project.
7 |
8 | .. __: https://mjw.woodcraft.me.uk/gomill/
9 |
10 |
11 | The differences are as follows:
12 |
13 | There is a new :func:`Sgf_game.from_bytes` classmethod, which behaves like the old
14 | :func:`!Sgf_game.from_string`.
15 |
16 | :func:`Sgf_game.from_string` now expects a Python 3 string (which contains
17 | Unicode characters), and forces the game's encoding to UTF-8.
18 |
19 | :meth:`Sgf_game.serialise` now returns a bytes object.
20 |
21 | :meth:`Tree_node.get_raw` and :meth:`Tree_node.set_raw` (and related methods)
22 | now use bytes objects.
23 |
24 | For Text and SimpleText properties, :meth:`Tree_node.get` and
25 | :meth:`Tree_node.set` use Python 3 strings.
26 |
27 |
28 | The exact rules for what 'raw' data is accepted for some property types have
29 | changed, due to changes in the Python language features used to implement the
30 | parser. In particular:
31 |
32 | * CESU-8 is no longer accepted when parsing data which purports to be UTF-8.
33 |
34 | * The raw values accepted for properties of type Real are now determined by
35 | Python 3's `float()`__ function rather than the platform libc (exactly what
36 | is accepted will depend on the Python minor version).
37 |
38 | * The interpreter for type Number is more lenient, accepting spaces anywhere
39 | in the string, and 'grouping underscores' from Python 3.6 on.
40 |
41 |
42 | .. __: https://docs.python.org/3/library/functions.html#float
43 |
--------------------------------------------------------------------------------
/doc/properties.rst:
--------------------------------------------------------------------------------
1 | List of SGF properties
2 | ======================
3 |
4 | Sgfmill knows about all general and Go-specific |sgf| properties defined in
5 | FF[4]:
6 |
7 | ====== ========================== ===================
8 | Id |sgf| type Meaning
9 | ====== ========================== ===================
10 | ``AB`` list of Stone Add Black
11 | ``AE`` list of Point Add Empty
12 | ``AN`` SimpleText Annotation
13 | ``AP`` SimpleText:SimpleText Application
14 | ``AR`` list of Point:Point Arrow
15 | ``AW`` list of Stone Add White
16 | ``B`` Move Black move
17 | ``BL`` Real Black time left
18 | ``BM`` Double Bad move
19 | ``BR`` SimpleText Black rank
20 | ``BT`` SimpleText Black team
21 | ``C`` Text Comment
22 | ``CA`` SimpleText Charset
23 | ``CP`` SimpleText Copyright
24 | ``CR`` list of Point Circle
25 | ``DD`` elist of Point Dim Points
26 | ``DM`` Double Even position
27 | ``DO`` None Doubtful
28 | ``DT`` SimpleText Date
29 | ``EV`` SimpleText Event
30 | ``FF`` Number File format
31 | ``FG`` None | Number:SimpleText Figure
32 | ``GB`` Double Good for Black
33 | ``GC`` Text Game comment
34 | ``GM`` Number Game
35 | ``GN`` SimpleText Game name
36 | ``GW`` Double Good for White
37 | ``HA`` Number Handicap
38 | ``HO`` Double Hotspot
39 | ``IT`` None Interesting
40 | ``KM`` Real Komi
41 | ``KO`` None Ko
42 | ``LB`` list of Point:SimpleText Label
43 | ``LN`` list of Point:Point Line
44 | ``MA`` list of Point Mark
45 | ``MN`` Number Set move number
46 | ``N`` SimpleText Node name
47 | ``OB`` Number Overtime stones left for Black
48 | ``ON`` SimpleText Opening
49 | ``OT`` SimpleText Overtime description
50 | ``OW`` Number Overtime stones left for White
51 | ``PB`` SimpleText Black player name
52 | ``PC`` SimpleText Place
53 | ``PL`` Colour Player to play
54 | ``PM`` Number Print move mode
55 | ``PW`` SimpleText White player name
56 | ``RE`` SimpleText Result
57 | ``RO`` SimpleText Round
58 | ``RU`` SimpleText Rules
59 | ``SL`` list of Point Selected
60 | ``SO`` SimpleText Source
61 | ``SQ`` list of Point Square
62 | ``ST`` Number Style
63 | ``SZ`` Number Size
64 | ``TB`` elist of Point Black territory
65 | ``TE`` Double Tesuji
66 | ``TM`` Real Time limit
67 | ``TR`` list of Point Triangle
68 | ``TW`` elist of Point White territory
69 | ``UC`` Double Unclear position
70 | ``US`` SimpleText User
71 | ``V`` Real Value
72 | ``VW`` elist of Point View
73 | ``W`` Move White move
74 | ``WL`` Real White time left
75 | ``WR`` SimpleText White rank
76 | ``WT`` SimpleText White team
77 | ====== ========================== ===================
78 |
79 |
--------------------------------------------------------------------------------
/doc/property_types.rst:
--------------------------------------------------------------------------------
1 | Property types
2 | ==============
3 |
4 | .. currentmodule:: sgfmill.sgf
5 |
6 | .. _basic_go_types:
7 |
8 | Basic Go types
9 | --------------
10 |
11 | Sgfmill represents Go colours and moves as follows:
12 |
13 | ======== ===========================================
14 | Name Possible values
15 | ======== ===========================================
16 | *colour* single-character string: ``'b'`` or ``'w'``
17 | *point* pair (*int*, *int*) of coordinates
18 | *move* *point* or ``None`` (for a pass)
19 | ======== ===========================================
20 |
21 | The terms *colour*, *point*, and *move* are used as above throughout this
22 | documentation (in particular, when describing parameters and return types).
23 |
24 | *colour* values are used to represent players, as well as stones on the board.
25 | (When a way to represent an empty point is needed, ``None`` is used.)
26 |
27 | *point* values are treated as (row, column). The bottom left is ``(0, 0)``
28 | (the same orientation as |gtp|, but not |sgf|). So the coordinates for a 9x9
29 | board are as follows::
30 |
31 | 9 (8,0) . . . . . (8,8)
32 | 8 . . . . . . . . .
33 | 7 . . . . . . . . .
34 | 6 . . . . . . . . .
35 | 5 . . . . . . . . .
36 | 4 . . . . . . . . .
37 | 3 . . . . . . . . .
38 | 2 . . . . . . . . .
39 | 1 (0,0) . . . . . (0,8)
40 | A B C D E F G H J
41 |
42 | There are functions in the :mod:`~sgfmill.common` module to convert between
43 | these coordinates and the conventional (``T19``\ -style) notation.
44 |
45 | Sgfmill is designed to work with square boards, up to size 26x26.
46 |
47 |
48 | .. _sgf_property_types:
49 |
50 | SGF property types
51 | ------------------
52 |
53 | The following table shows how |sgf| property types are represented as Python
54 | values (eg by the :meth:`Tree_node.get` and :meth:`Tree_node.set` methods).
55 |
56 | =========== ========================
57 | |sgf| type Python representation
58 | =========== ========================
59 | None ``True``
60 | Number int
61 | Real float
62 | Double ``1`` or ``2`` (int)
63 | Colour *colour*
64 | SimpleText string
65 | Text string
66 | Stone *point*
67 | Point *point*
68 | Move *move*
69 | =========== ========================
70 |
71 | Sgfmill doesn't distinguish the Point and Stone |sgf| property types. It
72 | rejects representations of 'pass' for the Point and Stone types, but accepts
73 | them for Move (this is not what is described in the |sgf| specification, but
74 | it does correspond to the properties in which 'pass' makes sense).
75 |
76 | Values of list or elist types are represented as Python lists. An empty elist
77 | is represented as an empty Python list (in contrast, the raw value is a list
78 | containing a single empty string).
79 |
80 | Values of compose types are represented as Python pairs (tuples of length
81 | two). ``FG`` values are either a pair (int, string) or ``None``.
82 |
83 | For Text and SimpleText values, :meth:`~Tree_node.get` and
84 | :meth:`~Tree_node.set` take care of escaping. You can store arbitrary strings
85 | in a Text value and retrieve them unchanged, with the following exceptions:
86 |
87 | * all linebreaks are normalised to ``\n``
88 |
89 | * whitespace other than line breaks is converted to a single space
90 |
91 | :meth:`~Tree_node.get` accepts compressed point lists, but
92 | :meth:`~Tree_node.set` never produces them (some |sgf| viewers still don't
93 | support them).
94 |
95 | In some cases, :meth:`~Tree_node.get` will accept values which are not
96 | strictly permitted in |sgf|, if there's a sensible way to interpret them. In
97 | particular, empty lists are accepted for all list types (not only elists).
98 |
99 | In some cases, :meth:`~Tree_node.set` will accept values which are not exactly
100 | in the Python representation listed, if there's a natural way to convert them
101 | to the |sgf| representation.
102 |
103 | Both :meth:`~Tree_node.get` and :meth:`~Tree_node.set` check that Point values
104 | are in range for the board size. Neither :meth:`~Tree_node.get` nor
105 | :meth:`~Tree_node.set` pays attention to range restrictions for values of type
106 | Number.
107 |
108 | Examples::
109 |
110 | >>> node.set('KO', True)
111 | >>> node.get_raw('KO')
112 | b''
113 | >>> node.set('HA', 3)
114 | >>> node.set('KM', 5.5)
115 | >>> node.set('GB', 2)
116 | >>> node.set('PL', 'w')
117 | >>> node.set('RE', 'W+R')
118 | >>> node.set('GC', 'Example game\n[for documentation]')
119 | >>> node.get_raw('GC')
120 | b'Example game\n[for documentation\\]'
121 | >>> node.set('B', (2, 3))
122 | >>> node.get_raw('B')
123 | b'dg'
124 | >>> node.set('LB', [((6, 0), "label 1"), ((6, 1), "label 2")])
125 | >>> node.get_raw_list('LB')
126 | [b'ac:label 1', b'bc:label 2']
127 |
128 |
129 |
130 |
--------------------------------------------------------------------------------
/doc/python-inv.txt:
--------------------------------------------------------------------------------
1 | # Sphinx inventory version 1
2 | # Project: Python
3 | # Version: 3
4 | datetime.date class library/datetime.html
5 |
--------------------------------------------------------------------------------
/doc/sgf.rst:
--------------------------------------------------------------------------------
1 | The :mod:`!sgf` module
2 | ----------------------
3 |
4 | .. module:: sgfmill.sgf
5 | :synopsis: High level SGF interface.
6 |
7 | The :mod:`!sgfmill.sgf` module provides the main high-level |sgf| interface.
8 |
9 | It defines two public classes:
10 |
11 | * :doc:`Sgf_game ` to represent an |sgf| ``GameTree``;
12 | * :doc:`Tree_node ` to represent an |sgf| ``Node``.
13 |
14 | .. toctree::
15 | :titlesonly:
16 | :hidden:
17 |
18 | sgf_games
19 | tree_nodes
20 |
--------------------------------------------------------------------------------
/doc/sgf_board_interface.rst:
--------------------------------------------------------------------------------
1 | The :mod:`!sgf_board_interface` module
2 | --------------------------------------
3 |
4 | .. module:: sgfmill.sgf_board_interface
5 | :synopsis: Description of the board interfaces required by sgf_moves.
6 |
7 | The :mod:`!sgfmill.sgf_board_interface` module defines two abstract classes,
8 | for documentation purposes:
9 |
10 | .. class:: Interface_for_get_setup_and_moves
11 |
12 | The board interface required by :func:`.sgf_moves.get_setup_and_moves()`.
13 |
14 | .. class:: Interface_for_set_initial_position
15 |
16 | The board interface required by :func:`.sgf_moves.set_initial_position()`.
17 |
18 |
19 |
20 | Use the 'source' links to the right to see the definitions.
21 |
22 |
--------------------------------------------------------------------------------
/doc/sgf_games.rst:
--------------------------------------------------------------------------------
1 | :class:`!Sgf_game` objects
2 | ^^^^^^^^^^^^^^^^^^^^^^^^^^
3 |
4 | .. currentmodule:: sgfmill.sgf
5 |
6 | |sgf| data is represented using :class:`!Sgf_game` objects. Each object
7 | represents the data for a single |sgf| file (corresponding to a ``GameTree``
8 | in the |sgf| spec). This is typically used to represent a single game,
9 | possibly with variations (but it could be something else, such as a problem
10 | set).
11 |
12 | .. contents:: Page contents
13 | :local:
14 | :backlinks: none
15 |
16 |
17 | Creating :class:`!Sgf_game`\ s
18 | """"""""""""""""""""""""""""""
19 |
20 | An :class:`!Sgf_game` can either be created from scratch or loaded from a
21 | string.
22 |
23 | To create one from scratch, instantiate an :class:`!Sgf_game` object directly:
24 |
25 | .. class:: Sgf_game(size[, encoding="UTF-8"])
26 |
27 | *size* is an integer from 1 to 26, indicating the board size.
28 |
29 | The optional *encoding* parameter specifies the :ref:`raw property encoding
30 | ` to use for the game.
31 |
32 | When a game is created this way, the following root properties are initially
33 | set: :samp:`FF[4]`, :samp:`GM[1]`, :samp:`SZ[{size}]`, and
34 | :samp:`CA[{encoding}]`.
35 |
36 | To create a game from existing |sgf| data, use the
37 | :func:`!Sgf_game.from_bytes` or :func:`!Sgf_game.from_string` classmethod:
38 |
39 | .. classmethod:: Sgf_game.from_bytes(bb[, override_encoding=None])
40 |
41 | :rtype: :class:`!Sgf_game`
42 |
43 | Creates an :class:`!Sgf_game` from the |sgf| data in *bb*, which must be
44 | a bytes-like object.
45 |
46 | Raises :exc:`ValueError` if it can't parse the data, or if the ``SZ`` or
47 | ``CA`` properties are unacceptable. No error is reported for other
48 | malformed property values. See also :doc:`parsing`.
49 |
50 | Assumes the data is in the encoding described by the ``CA`` property in the
51 | root node (defaulting to ``"ISO-8859-1"``), and uses that as the :ref:`raw
52 | property encoding `.
53 |
54 | But if *override_encoding* is present, assumes the data is in that encoding
55 | (no matter what the ``CA`` property says), and sets the ``CA`` property and
56 | raw property encoding to match.
57 |
58 | The board size is taken from the ``SZ`` propery in the root node
59 | (defaulting to ``19``). Board sizes greater than ``26`` are rejected.
60 |
61 |
62 | Example::
63 |
64 | g = sgf.Sgf_game.from_bytes(
65 | b"(;FF[4]GM[1]SZ[9]CA[UTF-8];B[ee];W[ge])",
66 | override_encoding="iso8859-1")
67 |
68 |
69 | .. classmethod:: Sgf_game.from_string(s)
70 |
71 | :rtype: :class:`!Sgf_game`
72 |
73 | Creates an :class:`!Sgf_game` from the |sgf| data in *s*, which must be a
74 | string.
75 |
76 | Raises :exc:`ValueError` if it can't parse the data, or if the ``SZ`` or
77 | ``CA`` properties are unacceptable. No error is reported for other
78 | malformed property values. See also :doc:`parsing`.
79 |
80 | The game's :ref:`raw property encoding ` and ``CA``
81 | property will be ``"UTF-8"`` (replacing any ``CA`` property present in the
82 | string).
83 |
84 | The board size is taken from the ``SZ`` propery in the root node
85 | (defaulting to ``19``). Board sizes greater than ``26`` are rejected.
86 |
87 | Example::
88 |
89 | g = sgf.Sgf_game.from_string(
90 | "(;FF[4]GM[1]SZ[9]CA[UTF-8];B[ee];W[ge])")
91 |
92 |
93 | Serialising :class:`!Sgf_game`\ s
94 | """""""""""""""""""""""""""""""""
95 |
96 | To retrieve the |sgf| data as bytes, use the :meth:`!serialise` method:
97 |
98 | .. method:: Sgf_game.serialise([wrap])
99 |
100 | :rtype: bytes
101 |
102 | Produces the |sgf| representation of the data in the :class:`!Sgf_game`.
103 |
104 | Returns a bytes object, in the encoding specified by the ``CA`` root
105 | property (defaulting to ``"ISO-8859-1"``).
106 |
107 | See :ref:`changing_ca` for details of the behaviour if the ``CA`` property
108 | is changed from its initial value.
109 |
110 | :meth:`!serialise` makes some effort to keep the output line length to no
111 | more than 79 bytes. Pass ``None`` in the *wrap* parameter to disable this
112 | behaviour, or pass an integer to specify a different limit.
113 |
114 |
115 | Accessing the game's nodes
116 | """"""""""""""""""""""""""
117 |
118 | The complete game tree is represented using :class:`Tree_node` objects, which
119 | are used to access the |sgf| properties. An :class:`!Sgf_game` always has at
120 | least one node, the :dfn:`root node`.
121 |
122 | .. method:: Sgf_game.get_root()
123 |
124 | :rtype: :class:`Tree_node`
125 |
126 | Returns the root node of the game tree.
127 |
128 | The complete game tree can be accessed through the root node, but the
129 | following convenience methods are also provided. They return the same
130 | :class:`Tree_node` objects that would be reached via the root node.
131 |
132 | Some of the convenience methods are for accessing the :dfn:`leftmost`
133 | variation of the game tree. This is the variation which appears first in the
134 | |sgf| ``GameTree``, often shown in graphical editors as the topmost horizontal
135 | line of nodes. In a game tree without variations, the leftmost variation is
136 | just the whole game.
137 |
138 |
139 | .. method:: Sgf_game.get_last_node()
140 |
141 | :rtype: :class:`Tree_node`
142 |
143 | Returns the last (leaf) node in the leftmost variation.
144 |
145 | .. method:: Sgf_game.get_main_sequence()
146 |
147 | :rtype: list of :class:`Tree_node` objects
148 |
149 | Returns the complete leftmost variation. The first element is the root
150 | node, and the last is a leaf.
151 |
152 | .. method:: Sgf_game.get_main_sequence_below(node)
153 |
154 | :rtype: list of :class:`Tree_node` objects
155 |
156 | Returns the leftmost variation beneath the :class:`Tree_node` *node*. The
157 | first element is the first child of *node*, and the last is a leaf.
158 |
159 | Note that this isn't necessarily part of the leftmost variation of the
160 | game as a whole.
161 |
162 | .. method:: Sgf_game.get_main_sequence_above(node)
163 |
164 | :rtype: list of :class:`Tree_node` objects
165 |
166 | Returns the partial variation leading to the :class:`Tree_node` *node*. The
167 | first element is the root node, and the last is the parent of *node*.
168 |
169 | .. method:: Sgf_game.extend_main_sequence()
170 |
171 | :rtype: :class:`Tree_node`
172 |
173 | Creates a new :class:`Tree_node`, adds it to the leftmost variation, and
174 | returns it.
175 |
176 | This is equivalent to
177 | :meth:`get_last_node`\ .\ :meth:`~Tree_node.new_child`
178 |
179 |
180 | Root node properties
181 | """"""""""""""""""""
182 |
183 | The root node contains global properties for the game tree, and typically also
184 | contains *game-info* properties. It sometimes also contains *setup* properties
185 | (for example, if the game does not begin with an empty board).
186 |
187 | Changing the ``FF`` and ``GM`` properties is permitted, but Sgfmill will carry
188 | on using the FF[4] and GM[1] (Go) rules.
189 |
190 | Changing ``SZ`` is not permitted (but if the size is 19 you may remove the
191 | property).
192 |
193 | Changing ``CA`` is permitted (this controls the encoding used by
194 | :meth:`~Sgf_game.serialise`).
195 |
196 | The following methods provide convenient access to some of the root node's
197 | |sgf| properties. The main difference between using these methods and using
198 | :meth:`~Tree_node.get` on the root node is that these methods return the
199 | appropriate default value if the property is not present.
200 |
201 | .. method:: Sgf_game.get_size()
202 |
203 | :rtype: integer
204 |
205 | Returns the board size (``19`` if the ``SZ`` root property isn't present).
206 |
207 | .. method:: Sgf_game.get_charset()
208 |
209 | :rtype: string
210 |
211 | Returns the effective value of the ``CA`` root property (``ISO-8859-1`` if
212 | the ``CA`` root property isn't present).
213 |
214 | The returned value is a codec name in normalised form, which may not be
215 | identical to the string returned by ``get_root().get("CA")``. Raises
216 | :exc:`ValueError` if the property value doesn't identify a Python codec.
217 |
218 | This gives the encoding that would be used by :meth:`serialise`. It is not
219 | necessarily the same as the :doc:`raw property encoding ` (use
220 | :meth:`~Tree_node.get_encoding` on the root node to retrieve that).
221 |
222 |
223 | .. method:: Sgf_game.get_komi()
224 |
225 | :rtype: float
226 |
227 | Returns the :term:`komi` (``0.0`` if the ``KM`` root property isn't
228 | present).
229 |
230 | Raises :exc:`ValueError` if the ``KM`` root property is present but
231 | malformed.
232 |
233 | .. method:: Sgf_game.get_handicap()
234 |
235 | :rtype: integer or ``None``
236 |
237 | Returns the number of handicap stones.
238 |
239 | Returns ``None`` if the ``HA`` root property isn't present, or if it has
240 | value zero (which isn't strictly permitted).
241 |
242 | Raises :exc:`ValueError` if the ``HA`` property is otherwise malformed.
243 |
244 | .. method:: Sgf_game.get_player_name(colour)
245 |
246 | :rtype: string or ``None``
247 |
248 | Returns the name of the specified player, or ``None`` if the required
249 | ``PB`` or ``PW`` root property isn't present.
250 |
251 | .. method:: Sgf_game.get_winner()
252 |
253 | :rtype: *colour*
254 |
255 | Returns the colour of the winning player.
256 |
257 | Returns ``None`` if the ``RE`` root property isn't present, or if neither
258 | player won.
259 |
260 | .. method:: Sgf_game.set_date([date])
261 |
262 | Sets the ``DT`` root property, to a single date.
263 |
264 | If *date* is specified, it should be a :class:`datetime.date`. Otherwise
265 | the current date is used.
266 |
267 | (|sgf| allows ``DT`` to be rather more complicated than a single date, so
268 | there's no corresponding get_date() method.)
269 |
270 |
271 |
--------------------------------------------------------------------------------
/doc/sgf_moves.rst:
--------------------------------------------------------------------------------
1 | The :mod:`!sgf_moves` module
2 | ----------------------------
3 |
4 | .. module:: sgfmill.sgf_moves
5 | :synopsis: Higher-level processing of moves and positions from SGF games.
6 |
7 | The :mod:`!sgfmill.sgf_moves` module contains some higher-level functions for
8 | processing moves and game positions.
9 |
10 | (They are the sort of thing you might need to implement 'load SGF' and 'save
11 | as SGF' features in a Go-playing program.)
12 |
13 |
14 | .. function:: get_setup_and_moves(sgf_game[, board])
15 |
16 | :rtype: tuple (:class:`.Board`, list of tuples (*colour*, *move*))
17 |
18 | Returns the initial setup and the following moves from an
19 | :class:`.Sgf_game`.
20 |
21 | The board represents the position described by ``AB`` and/or ``AW``
22 | properties in the |sgf| game's root node. :exc:`ValueError` is raised if
23 | this position isn't legal.
24 |
25 | The moves are from the game's leftmost variation. Doesn't check that the
26 | moves are legal.
27 |
28 | Raises :exc:`ValueError` if the game has structure it doesn't support.
29 |
30 | Currently doesn't support ``AB``/``AW``/``AE`` properties after the root
31 | node.
32 |
33 | If the optional *board* parameter is provided, it must be an empty
34 | :class:`.Board` of the right size; the same object will be returned. This
35 | option is provided so you can use a different Board class (see
36 | :class:`.Interface_for_get_setup_and_moves` for what it needs to implement).
37 |
38 | See also the :script:`show_sgf.py` example script.
39 |
40 |
41 | .. function:: set_initial_position(sgf_game, board)
42 |
43 | Adds ``AB``/``AW``/``AE`` properties to an :class:`.Sgf_game`'s root node,
44 | to reflect the position from a :class:`.Board`.
45 |
46 | Replaces any existing ``AB``/``AW``/``AE`` properties in the root node.
47 |
48 | If you wish to use your own board class, see
49 | :class:`.Interface_for_set_initial_position` for what it needs to
50 | implement.
51 |
52 |
53 | .. function:: indicate_first_player(sgf_game)
54 |
55 | Adds a ``PL`` property to an :class:`.Sgf_game`'s root node if appropriate,
56 | to indicate which colour is first to play.
57 |
58 | Looks at the first child of the root to see who the first player is, and
59 | sets ``PL`` it isn't the expected player (Black normally, but White if
60 | there is a handicap), or if there are non-handicap setup stones.
61 |
62 |
--------------------------------------------------------------------------------
/doc/sgfmill_package.rst:
--------------------------------------------------------------------------------
1 | The :mod:`sgfmill` package
2 | ==========================
3 |
4 | All Sgfmill code is contained in modules under the :mod:`!sgfmill` package.
5 |
6 | The package module itself defines only a single constant:
7 |
8 | .. module:: sgfmill
9 | :synopsis: Tools for testing and tuning Go-playing programs.
10 |
11 | .. data:: __version__
12 |
13 | The library version, as a string (like ``"1.0"``).
14 |
15 |
16 | .. toctree::
17 | :hidden:
18 | :maxdepth: 3
19 | :titlesonly:
20 |
21 | sgf
22 | sgf_moves
23 | sgf_board_interface
24 | boards
25 | ascii_boards
26 | common
27 |
28 |
29 | Modules
30 | -------
31 |
32 | The package includes the following modules:
33 |
34 | .. the descriptions here should normally match the module :synopsis:, and
35 | therefore the module index.
36 |
37 | ========================================= ========================================================================
38 | |sgf|-specific
39 | ========================================= ========================================================================
40 | :mod:`~sgfmill.sgf` High level |sgf| interface.
41 | :mod:`~sgfmill.sgf_moves` Higher-level processing of moves and positions from |sgf| games.
42 | :mod:`~sgfmill.sgf_board_interface` Go-board interface required by :mod:`!sgf_moves`.
43 | :mod:`~!sgfmill.sgf_grammar` The |sgf| parser.
44 | :mod:`~!sgfmill.sgf_properties` Interpreting |sgf| property values.
45 | ========================================= ========================================================================
46 |
47 | ========================================= ========================================================================
48 | Go-related support code
49 | ========================================= ========================================================================
50 | :mod:`~sgfmill.boards` Go board representation.
51 | :mod:`~sgfmill.ascii_boards` ASCII Go board diagrams.
52 | :mod:`~sgfmill.common` Go-related utility functions.
53 | ========================================= ========================================================================
54 |
55 | The main public interface is in the :mod:`~sgfmill.sgf` module.
56 |
57 | The :mod:`~sgfmill.sgf_moves` module contains some higher-level functions for
58 | processing moves and positions.
59 |
60 | The :mod:`~sgfmill.sgf_board_interface` module defines an abstract board
61 | interface required by functions in :mod:`!sgf_moves` (and implemented by
62 | :class:`boards.Board`).
63 |
64 | The :mod:`!sgf_grammar` and :mod:`!sgf_properties` modules are
65 | used to implement the :mod:`!sgf` module, and are not currently documented.
66 |
67 | The :mod:`~sgfmill.boards` module provides a Go board representation, used by
68 | :mod:`!sgf_moves`.
69 |
70 | The :mod:`~sgfmill.ascii_boards` module supports ASCII board diagrams, used by some
71 | example scripts and the testsuite.
72 |
73 | The :mod:`~sgfmill.common` module provides a few Go-related utility functions,
74 | mostly used only by :mod:`~sgfmill.ascii_boards`.
75 |
76 |
--------------------------------------------------------------------------------
/doc/tree_nodes.rst:
--------------------------------------------------------------------------------
1 | :class:`!Tree_node` objects
2 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^
3 |
4 | .. currentmodule:: sgfmill.sgf
5 |
6 | .. class:: Tree_node
7 |
8 | A Tree_node object represents a single node from an |sgf| file.
9 |
10 | Don't instantiate :class:`!Tree_node` objects directly; retrieve them from
11 | :class:`Sgf_game` objects.
12 |
13 | .. contents:: Page contents
14 | :local:
15 | :backlinks: none
16 |
17 |
18 | Attributes
19 | """"""""""
20 |
21 | Tree_node objects have the following attributes (which should be treated as
22 | read-only):
23 |
24 | .. attribute:: owner
25 |
26 | The :class:`Sgf_game` that the node belongs to.
27 |
28 | .. attribute:: parent
29 |
30 | The node's parent :class:`!Tree_node` (``None`` for the root node).
31 |
32 |
33 | Tree navigation
34 | """""""""""""""
35 |
36 | A :class:`!Tree_node` acts as a list-like container of its children: it can be
37 | indexed, sliced, and iterated over like a list, and it supports the `index`__
38 | method.
39 |
40 | A :class:`!Tree_node` with no children is treated as having truth value false.
41 |
42 | For example, to find all leaf nodes::
43 |
44 | def print_leaf_comments(node):
45 | if node:
46 | for child in node:
47 | print_leaf_comments(child)
48 | else:
49 | if node.has_property("C"):
50 | print(node.get("C"))
51 | else:
52 | print("--")
53 |
54 | .. __: https://docs.python.org/3/library/stdtypes.html#mutable-sequence-types
55 |
56 |
57 | Property access
58 | """""""""""""""
59 |
60 | Each node holds a number of :dfn:`properties`. Each property is identified by
61 | a short string called the :dfn:`PropIdent`, eg ``"SZ"`` or ``"B"``. See
62 | :doc:`properties` for a list of the standard properties. See the
63 | :term:`SGF` specification for full details. See :doc:`parsing` for
64 | restrictions on well-formed *PropIdents*.
65 |
66 | Sgfmill doesn't enforce |sgf|'s restrictions on where properties can appear
67 | (eg, the distinction between *setup* and *move* properties).
68 |
69 | The principal methods for accessing the node's properties are:
70 |
71 | .. method:: Tree_node.get(identifier)
72 |
73 | Returns a native Python representation of the value of the property whose
74 | *PropIdent* is *identifier*.
75 |
76 | Raises :exc:`KeyError` if the property isn't present.
77 |
78 | Raises :exc:`ValueError` if it detects that the property value is
79 | malformed.
80 |
81 | See :ref:`sgf_property_types` for details of how property values are
82 | represented in Python.
83 |
84 | See :doc:`properties` for a list of the known properties. Any other
85 | property is treated as having type Text.
86 |
87 | .. method:: Tree_node.set(identifier, value)
88 |
89 | Sets the value of the property whose *PropIdent* is *identifier*.
90 |
91 | *value* should be a native Python representation of the required property
92 | value (as returned by :meth:`get`).
93 |
94 | Raises :exc:`ValueError` if the identifier isn't a well-formed *PropIdent*,
95 | or if the property value isn't acceptable.
96 |
97 | See :ref:`sgf_property_types` for details of how property values should be
98 | represented in Python.
99 |
100 | See :doc:`properties` for a list of the known properties. Setting
101 | nonstandard properties is permitted; they are treated as having type Text.
102 |
103 | .. method:: Tree_node.unset(identifier)
104 |
105 | Removes the property whose *PropIdent* is *identifier* from the node.
106 |
107 | Raises :exc:`KeyError` if the property isn't currently present.
108 |
109 | .. method:: Tree_node.has_property(identifier)
110 |
111 | :rtype: bool
112 |
113 | Checks whether the property whose *PropIdent* is *identifier* is present.
114 |
115 | .. method:: Tree_node.properties()
116 |
117 | :rtype: list of strings
118 |
119 | Lists the properties which are present in the node.
120 |
121 | Returns a list of *PropIdents*, in unspecified order.
122 |
123 | .. method:: Tree_node.find_property(identifier)
124 |
125 | Returns the value of the property whose *PropIdent* is *identifier*,
126 | looking in the node's ancestors if necessary.
127 |
128 | This is intended for use with properties of type *game-info*, and with
129 | properties which have the *inherit* attribute.
130 |
131 | It looks first in the node itself, then in its parent, and so on up to the
132 | root, returning the first value it finds. Otherwise the behaviour is the
133 | same as :meth:`get`.
134 |
135 | Raises :exc:`KeyError` if no node defining the property is found.
136 |
137 |
138 | .. method:: Tree_node.find(identifier)
139 |
140 | :rtype: :class:`!Tree_node` or ``None``
141 |
142 | Returns the nearest node defining the property whose *PropIdent* is
143 | *identifier*.
144 |
145 | Searches in the same way as :meth:`find_property`, but returns the node
146 | rather than the property value. Returns ``None`` if no node defining the
147 | property is found.
148 |
149 |
150 | Convenience methods for properties
151 | """"""""""""""""""""""""""""""""""
152 |
153 | The following convenience methods are also provided, for more flexible access
154 | to a few of the most important properties:
155 |
156 | .. method:: Tree_node.get_move()
157 |
158 | :rtype: tuple (*colour*, *move*)
159 |
160 | Indicates which of the the ``B`` or ``W`` properties is present, and
161 | returns its value.
162 |
163 | Returns (``None``, ``None``) if neither property is present.
164 |
165 | .. method:: Tree_node.set_move(colour, move)
166 |
167 | Sets the ``B`` or ``W`` property. If the other property is currently
168 | present, it is removed.
169 |
170 | Sgfmill doesn't attempt to ensure that moves are legal.
171 |
172 | .. method:: Tree_node.get_setup_stones()
173 |
174 | :rtype: tuple (set of *points*, set of *points*, set of *points*)
175 |
176 | Returns the settings of the ``AB``, ``AW``, and ``AE`` properties.
177 |
178 | The tuple elements represent black, white, and empty points respectively.
179 | If a property is missing, the corresponding set is empty.
180 |
181 | .. method:: Tree_node.set_setup_stones(black, white[, empty])
182 |
183 | Sets the ``AB``, ``AW``, and ``AE`` properties.
184 |
185 | Each parameter should be a sequence or set of *points*. If a parameter
186 | value is empty (or, in the case of *empty*, if the parameter is
187 | omitted) the corresponding property will be unset.
188 |
189 | .. method:: Tree_node.has_setup_stones()
190 |
191 | :rtype: bool
192 |
193 | Returns ``True`` if the ``AB``, ``AW``, or ``AE`` property is present.
194 |
195 | .. method:: Tree_node.add_comment_text(text)
196 |
197 | If the ``C`` property isn't already present, adds it with the value given
198 | by the string *text*.
199 |
200 | Otherwise, appends *text* to the existing ``C`` property value, preceded by
201 | two newlines.
202 |
203 |
204 | Board size and raw property encoding
205 | """"""""""""""""""""""""""""""""""""
206 |
207 | Each :class:`!Tree_node` knows its game's board size, and its :ref:`raw
208 | property encoding ` (because these are needed to
209 | interpret property values). They can be retrieved using the following methods:
210 |
211 | .. method:: Tree_node.get_size()
212 |
213 | :rtype: int
214 |
215 | .. method:: Tree_node.get_encoding()
216 |
217 | :rtype: string
218 |
219 | This returns the name of the raw property encoding (in a normalised form,
220 | which may not be the same as the string originally used to specify the
221 | encoding).
222 |
223 | An attempt to change the value of the ``SZ`` property so that it doesn't match
224 | the board size will raise :exc:`ValueError` (even if the node isn't the root).
225 |
226 |
227 | Access to raw property values
228 | """""""""""""""""""""""""""""
229 |
230 | Raw property values are bytes objects, containing the exact bytes that go
231 | between the ``[`` and ``]`` in the |sgf| file. They should be treated as being
232 | encoded in the node's :ref:`raw property encoding `
233 | (but there is no guarantee that they hold properly encoded data).
234 |
235 | The following methods are provided for access to raw property values. They can
236 | be used to access malformed values, or to avoid the standard escape processing
237 | and whitespace conversion for Text and SimpleText values.
238 |
239 | When setting raw property values, any data that is a well formed |sgf|
240 | *PropValue* is accepted: that is, any byte-string that that doesn't contain an
241 | unescaped ``]`` or end with an unescaped ``\``. There is no check that the
242 | string is properly encoded in the raw property encoding.
243 |
244 | .. method:: Tree_node.get_raw_list(identifier)
245 |
246 | :rtype: nonempty list of bytes objects
247 |
248 | Returns the raw values of the property whose *PropIdent* is *identifier*.
249 |
250 | Raises :exc:`KeyError` if the property isn't currently present.
251 |
252 | If the property value is an empty elist, returns a list containing a single
253 | empty bytes object.
254 |
255 | .. method:: Tree_node.get_raw(identifier)
256 |
257 | :rtype: bytes object
258 |
259 | Returns the raw value of the property whose *PropIdent* is *identifier*.
260 |
261 | Raises :exc:`KeyError` if the property isn't currently present.
262 |
263 | If the property has multiple `PropValue`\ s, returns the first. If the
264 | property value is an empty elist, returns an empty bytes object.
265 |
266 | .. method:: Tree_node.get_raw_property_map(identifier)
267 |
268 | :rtype: dict: string → list of bytes objects
269 |
270 | Returns a dict mapping *PropIdents* to lists of raw values.
271 |
272 | Returns the same dict object each time it's called.
273 |
274 | Treat the returned dict object as read-only.
275 |
276 | .. method:: Tree_node.set_raw_list(identifier, values)
277 |
278 | Sets the raw values of the property whose *PropIdent* is *identifier*.
279 |
280 | *values* must be a nonempty list of bytes objects. To specify an empty
281 | elist, pass a list containing a single empty bytes object.
282 |
283 | Raises :exc:`ValueError` if the identifier isn't a well-formed *PropIdent*,
284 | or if any value isn't a well-formed *PropValue*.
285 |
286 | .. method:: Tree_node.set_raw(identifier, value)
287 |
288 | Sets the raw value of the property whose *PropIdent* is *identifier*.
289 |
290 | *value* must be a bytes object.
291 |
292 | Raises :exc:`ValueError` if the identifier isn't a well-formed *PropIdent*,
293 | or if the value isn't a well-formed *PropValue*.
294 |
295 |
296 | Tree manipulation
297 | """""""""""""""""
298 |
299 | The following methods are provided for manipulating the tree:
300 |
301 | .. method:: Tree_node.new_child([index])
302 |
303 | :rtype: :class:`!Tree_node`
304 |
305 | Creates a new :class:`!Tree_node` and adds it to the tree as this node's
306 | last child.
307 |
308 | If the optional integer *index* parameter is present, the new node is
309 | inserted in the list of children at the specified index instead (with the
310 | same behaviour as :meth:`!list.insert`).
311 |
312 | Returns the new node.
313 |
314 | .. method:: Tree_node.delete()
315 |
316 | Removes the node from the tree (along with all its descendents).
317 |
318 | Raises :exc:`ValueError` if called on the root node.
319 |
320 | You should not continue to use a node which has been removed from its tree.
321 |
322 | .. method:: Tree_node.reparent(new_parent[, index])
323 |
324 | Moves the node from one part of the tree to another (along with all its
325 | descendents).
326 |
327 | *new_parent* must be a node belonging to the same game.
328 |
329 | Raises :exc:`ValueError` if the operation would create a loop in the tree
330 | (ie, if *new_parent* is the node being moved or one of its descendents).
331 |
332 | If the optional integer *index* parameter is present, the new node is
333 | inserted in the new parent's list of children at the specified index;
334 | otherwise it is placed at the end.
335 |
336 | This method can be used to reorder variations. For example, to make a node
337 | the leftmost variation of its parent::
338 |
339 | node.reparent(node.parent, 0)
340 |
341 |
--------------------------------------------------------------------------------
/examples/show_sgf.py:
--------------------------------------------------------------------------------
1 | """Show the position from an SGF file.
2 |
3 | This demonstrates the sgf and ascii_boards modules.
4 |
5 | """
6 |
7 | import sys
8 | from optparse import OptionParser
9 |
10 | from sgfmill import ascii_boards
11 | from sgfmill import sgf
12 | from sgfmill import sgf_moves
13 |
14 | def show_sgf_file(pathname, move_number):
15 | f = open(pathname, "rb")
16 | sgf_src = f.read()
17 | f.close()
18 | try:
19 | sgf_game = sgf.Sgf_game.from_bytes(sgf_src)
20 | except ValueError:
21 | raise Exception("bad sgf file")
22 |
23 | try:
24 | board, plays = sgf_moves.get_setup_and_moves(sgf_game)
25 | except ValueError as e:
26 | raise Exception(str(e))
27 | if move_number is not None:
28 | move_number = max(0, move_number-1)
29 | plays = plays[:move_number]
30 |
31 | for colour, move in plays:
32 | if move is None:
33 | continue
34 | row, col = move
35 | try:
36 | board.play(row, col, colour)
37 | except ValueError:
38 | raise Exception("illegal move in sgf file")
39 |
40 | print(ascii_boards.render_board(board))
41 | print()
42 |
43 | _description = """\
44 | Show the position from an SGF file. If a move number is specified, the position
45 | before that move is shown (this is to match the behaviour of GTP loadsgf).
46 | """
47 |
48 | def main(argv):
49 | parser = OptionParser(usage="%prog [move number]",
50 | description=_description)
51 | opts, args = parser.parse_args(argv)
52 | if not args:
53 | parser.error("not enough arguments")
54 | pathname = args[0]
55 | if len(args) > 2:
56 | parser.error("too many arguments")
57 | if len(args) == 2:
58 | try:
59 | move_number = int(args[1])
60 | except ValueError:
61 | parser.error("invalid integer value: %s" % args[1])
62 | else:
63 | move_number = None
64 | try:
65 | show_sgf_file(pathname, move_number)
66 | except Exception as e:
67 | print("show_sgf:", str(e), file=sys.stderr)
68 | sys.exit(1)
69 |
70 | if __name__ == "__main__":
71 | main(sys.argv[1:])
72 |
73 |
--------------------------------------------------------------------------------
/examples/split_sgf_collection.py:
--------------------------------------------------------------------------------
1 | """Split an SGF collection into separate files.
2 |
3 | This demonstrates the parsing functions from the sgf_grammar module.
4 |
5 | """
6 |
7 | import os
8 | import sys
9 | from optparse import OptionParser
10 |
11 | from sgfmill import sgf_grammar
12 | from sgfmill import sgf
13 |
14 | def split_sgf_collection(pathname):
15 | f = open(pathname, "rb")
16 | sgf_src = f.read()
17 | f.close()
18 | dirname, basename = os.path.split(pathname)
19 | root, ext = os.path.splitext(basename)
20 | dstdir = os.path.join(dirname, root + "_files")
21 | os.mkdir(dstdir)
22 | try:
23 | coarse_games = sgf_grammar.parse_sgf_collection(sgf_src)
24 | except ValueError as e:
25 | raise Exception("error parsing file: %s" % e)
26 | for i, coarse_game in enumerate(coarse_games):
27 | sgf_game = sgf.Sgf_game.from_coarse_game_tree(coarse_game)
28 | sgf_game.get_root().add_comment_text(
29 | "Split from %s (game %d)" % (basename, i+1))
30 | split_pathname = os.path.join(dstdir, "%s_%d%s" % (root, i+1, ext))
31 | with open(split_pathname, "wb") as f:
32 | f.write(sgf_game.serialise())
33 |
34 |
35 | _description = """\
36 | Split a file containing an SGF game collection into multiple files.
37 | """
38 |
39 | def main(argv):
40 | parser = OptionParser(usage="%prog ",
41 | description=_description)
42 | opts, args = parser.parse_args(argv)
43 | if not args:
44 | parser.error("not enough arguments")
45 | pathname = args[0]
46 | if len(args) > 1:
47 | parser.error("too many arguments")
48 | try:
49 | split_sgf_collection(pathname)
50 | except Exception as e:
51 | print("sgf_splitter:", str(e), file=sys.stderr)
52 | sys.exit(1)
53 |
54 | if __name__ == "__main__":
55 | main(sys.argv[1:])
56 |
57 |
--------------------------------------------------------------------------------
/release_sgfmill.py:
--------------------------------------------------------------------------------
1 | """Package a sgfmill release.
2 |
3 | Requires a release_sgfmill.conf file, eg:
4 |
5 | «
6 | # All paths are relative to this config file
7 | repo_dir = "."
8 | working_dir = "./tmp/release"
9 | log_pathname = "./tmp/release/release.log"
10 | target_dir = "./tmp/release"
11 | html_files_to_remove = [
12 | ".buildinfo",
13 | ]
14 | »
15 |
16 | """
17 |
18 | import os
19 | import re
20 | import shutil
21 | import sys
22 | from optparse import OptionParser
23 | from subprocess import check_call, check_output, CalledProcessError, Popen, PIPE
24 |
25 | class Failure(Exception):
26 | pass
27 |
28 | def read_python_file(pathname):
29 | dummy = {}
30 | result = {}
31 | with open(pathname, 'rb') as f:
32 | bb = f.read()
33 | exec(bb, dummy, result)
34 | return result
35 |
36 | def is_safe_tag(s):
37 | if s in (".", ".."):
38 | return False
39 | return bool(re.search(r"\A[-_.a-zA-Z0-9]+\Z", s))
40 |
41 | def is_acceptable_version(s):
42 | if s in (".", ".."):
43 | return False
44 | if len(s) > 12:
45 | return False
46 | return bool(re.search(r"\A[-_.a-zA-Z0-9]+\Z", s))
47 |
48 | def export_tag(dst, repo_dir, tag):
49 | """Export from the git tag.
50 |
51 | repo_dir -- git repository to export from (must contain .git)
52 | tag -- tag to export
53 | dst -- directory to export into
54 |
55 | The exported tree will be placed in a directory named inside .
56 |
57 | All files have the timestamp of the commit referred to by the tag.
58 |
59 | """
60 | if not os.path.isdir(os.path.join(repo_dir, ".git")):
61 | raise Failure("No .git repo in %s" % repo_dir)
62 | try:
63 | check_call("git archive --remote=%s --prefix=%s/ %s | tar -C %s -xf -" %
64 | (repo_dir, tag, tag, dst),
65 | shell=True)
66 | except CalledProcessError:
67 | raise Failure("export failed")
68 |
69 | def prepare_dir():
70 | """Make any required changes in the exported distribution directory.
71 |
72 | cwd must be the distribution directory (the project root).
73 |
74 | """
75 | os.rename("README.sdist.txt", "README.txt")
76 | os.remove("README.rst")
77 |
78 | def get_version():
79 | """Obtain the sgfmill version from setup.py."""
80 | try:
81 | output = check_output("python setup.py --version".split(),
82 | universal_newlines=True)
83 | except CalledProcessError:
84 | raise Failure("'setup.py --version' failed")
85 | version = output.strip()
86 | if not is_acceptable_version(version):
87 | raise Failure("bad version: %s" % repr(version))
88 | return version
89 |
90 | def make_sdist(version, logfile):
91 | """Run 'setup.py sdist'.
92 |
93 | cwd must be the distribution directory (the project root).
94 |
95 | Returns the pathname of the sdist tar.gz, relative to the distribution
96 | directory.
97 |
98 | """
99 | try:
100 | check_call("python setup.py sdist".split(), stdout=logfile)
101 | except CalledProcessError:
102 | raise Failure("'setup.py sdist' failed")
103 | result = os.path.join("dist", "sgfmill-%s.tar.gz" % version)
104 | if not os.path.exists(result):
105 | raise Failure("'setup.py sdist' did not create %s" % result)
106 | return result
107 |
108 | def make_wheel(version, logfile):
109 | """Run 'setup.py bdist_wheel'.
110 |
111 | cwd must be the distribution directory (the project root).
112 |
113 | Returns the pathname of the sdist tar.gz, relative to the distribution
114 | directory.
115 |
116 | """
117 | try:
118 | check_call("python setup.py bdist_wheel".split(), stdout=logfile)
119 | except CalledProcessError:
120 | raise Failure("'setup.py bdist_wheel' failed")
121 | result = os.path.join("dist", "sgfmill-%s-py3-none-any.whl" % version)
122 | if not os.path.exists(result):
123 | raise Failure("'setup.py bdist_wheel' did not create %s" % result)
124 | return result
125 |
126 | def make_sphinx(version, logfile, html_files_to_remove):
127 | """Run build_docs and make a tarball.
128 |
129 | cwd must be the distribution directory (the project root).
130 |
131 | Returns the pathname of the docs tar.gz, relative to the distribution
132 | directory.
133 |
134 | """
135 | try:
136 | check_call(["./build_docs"], stdout=logfile)
137 | except CalledProcessError:
138 | raise Failure("'build_docs' failed")
139 | htmlpath = os.path.join("doc", "_build", "html")
140 | for filename in html_files_to_remove:
141 | os.remove(os.path.join(htmlpath, filename))
142 | os.rename(htmlpath, "sgfmill-doc-%s" % version)
143 | try:
144 | check_call(("tar -czf sgfmill-doc-%s.tar.gz sgfmill-doc-%s" %
145 | (version, version)).split())
146 | except CalledProcessError:
147 | raise Failure("tarring up sgfmill-doc failed")
148 | return "sgfmill-doc-%s.tar.gz" % version
149 |
150 | def do_release(tag, config_pathname):
151 | config_dir = os.path.abspath(os.path.dirname(config_pathname))
152 | os.chdir(config_dir)
153 |
154 | try:
155 | config = read_python_file(config_pathname)
156 | except Exception as e:
157 | raise Failure("error reading config file:\n%s" % e)
158 |
159 | export_dir = os.path.join(config['working_dir'], tag)
160 | if os.path.exists(export_dir):
161 | shutil.rmtree(export_dir)
162 | export_tag(config['working_dir'], config['repo_dir'], tag)
163 | logfile = open(config['log_pathname'], "w")
164 | os.chdir(export_dir)
165 | prepare_dir()
166 | version = get_version()
167 | sdist_pathname = make_sdist(version, logfile)
168 | wheel_pathname = make_wheel(version, logfile)
169 | docs_pathname = make_sphinx(version, logfile,
170 | config['html_files_to_remove'])
171 | os.chdir(config_dir)
172 | logfile.close()
173 | sdist_dst = os.path.join(config['target_dir'],
174 | os.path.basename(sdist_pathname))
175 | wheel_dst = os.path.join(config['target_dir'],
176 | os.path.basename(wheel_pathname))
177 | docs_dst = os.path.join(config['target_dir'],
178 | os.path.basename(docs_pathname))
179 | if os.path.exists(sdist_dst):
180 | os.remove(sdist_dst)
181 | if os.path.exists(wheel_dst):
182 | os.remove(wheel_dst)
183 | if os.path.exists(docs_dst):
184 | os.remove(docs_dst)
185 | shutil.move(os.path.join(export_dir, sdist_pathname), sdist_dst)
186 | shutil.move(os.path.join(export_dir, wheel_pathname), wheel_dst)
187 | shutil.move(os.path.join(export_dir, docs_pathname), docs_dst)
188 | shutil.rmtree(export_dir)
189 |
190 |
191 | USAGE = """\
192 | %(prog)s \
193 | """
194 |
195 | def main(argv):
196 | parser = OptionParser(usage=USAGE)
197 | opts, args = parser.parse_args(argv)
198 | if len(args) != 1:
199 | parser.error("wrong number of arguments")
200 | tag = args[0]
201 | if not is_safe_tag(tag):
202 | parser.error("ill-formed tag")
203 | config_pathname = os.path.join(
204 | os.path.abspath(os.path.dirname(__file__)), "release_sgfmill.conf")
205 | try:
206 | if not os.path.exists(config_pathname):
207 | raise Failure("config file %s does not exist" % config_pathname)
208 | do_release(tag, config_pathname)
209 | except (OSError, Failure) as e:
210 | print("release_sgfmill.py: %s" % e, file=sys.stderr)
211 | sys.exit(1)
212 |
213 | if __name__ == "__main__":
214 | main(sys.argv[1:])
215 |
--------------------------------------------------------------------------------
/run_sgfmill_testsuite:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | python3 -m sgfmill_tests.run_sgfmill_testsuite "$@"
3 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup
2 |
3 | import sgfmill
4 |
5 | SGFMILL_URL = "https://mjw.woodcraft.me.uk/sgfmill/"
6 | VERSION = sgfmill.__version__
7 |
8 | LONG_DESCRIPTION = """\
9 | Sgfmill is a Python library for reading and writing Go game records
10 | using Smart Game Format (SGF).
11 |
12 | It supports:
13 |
14 | * loading SGF game records to make a Python object representation
15 | * creating SGF game objects from scratch
16 | * setting properties and manipulating the tree structure
17 | * serialising game records to SGF data
18 | * applying setup stones and moves to a Go board position
19 |
20 | Download: %(SGFMILL_URL)sdownload/sgfmill-%(VERSION)s.tar.gz
21 |
22 | Documentation: %(SGFMILL_URL)sdownload/sgfmill-doc-%(VERSION)s.tar.gz
23 |
24 | Online Documentation: %(SGFMILL_URL)sdoc/%(VERSION)s/
25 |
26 | Changelog: %(SGFMILL_URL)sdoc/%(VERSION)s/changes.html
27 |
28 | Github: https://github.com/mattheww/sgfmill
29 |
30 | """ % vars()
31 |
32 | setup(name='sgfmill',
33 | version=VERSION,
34 | url=SGFMILL_URL,
35 | description=\
36 | "Library for reading and writing files using Smart Game Format (SGF).",
37 | long_description=LONG_DESCRIPTION,
38 | author="Matthew Woodcraft",
39 | author_email="matthew@woodcraft.me.uk",
40 | packages=['sgfmill'],
41 | zip_safe=False,
42 | classifiers=[
43 | "Development Status :: 5 - Production/Stable",
44 | "Intended Audience :: Developers",
45 | "License :: OSI Approved :: MIT License",
46 | "Natural Language :: English",
47 | "Programming Language :: Python :: 3",
48 | "Programming Language :: Python :: 3.2",
49 | "Programming Language :: Python :: 3.3",
50 | "Programming Language :: Python :: 3.4",
51 | "Programming Language :: Python :: 3.5",
52 | "Programming Language :: Python :: 3.6",
53 | "Programming Language :: Python",
54 | "Topic :: Games/Entertainment :: Board Games",
55 | "Topic :: Software Development :: Libraries :: Python Modules",
56 | ],
57 | keywords="go,baduk,weiqi,sgf",
58 | license="MIT",
59 | )
60 |
61 |
--------------------------------------------------------------------------------
/sgfmill/__init__.py:
--------------------------------------------------------------------------------
1 | __version__ = "1.1.1"
2 |
--------------------------------------------------------------------------------
/sgfmill/ascii_boards.py:
--------------------------------------------------------------------------------
1 | """ASCII board representation."""
2 |
3 | from . import boards
4 | from .common import column_letters
5 |
6 | def render_grid(point_formatter, size):
7 | """Render a board-shaped grid as a list of strings.
8 |
9 | point_formatter -- function (row, col) -> string of length 2.
10 |
11 | Returns a list of strings.
12 |
13 | """
14 | column_header_string = " ".join(column_letters[i] for i in range(size))
15 | result = []
16 | if size > 9:
17 | rowstart = "%2d "
18 | padding = " "
19 | else:
20 | rowstart = "%d "
21 | padding = ""
22 | for row in range(size-1, -1, -1):
23 | result.append(rowstart % (row+1) +
24 | " ".join(point_formatter(row, col)
25 | for col in range(size)))
26 | result.append(padding + " " + column_header_string)
27 | return result
28 |
29 | _point_strings = {
30 | None : " .",
31 | 'b' : " #",
32 | 'w' : " o",
33 | }
34 |
35 | def render_board(board):
36 | """Render an sgfmill Board in ascii.
37 |
38 | Returns a string without final newline.
39 |
40 | """
41 | def format_pt(row, col):
42 | return _point_strings.get(board.get(row, col), " ?")
43 | return "\n".join(render_grid(format_pt, board.side))
44 |
45 | def interpret_diagram(diagram, size, board=None):
46 | """Set up the position from a diagram.
47 |
48 | diagram -- board representation as from render_board()
49 | size -- int
50 |
51 | Returns a Board.
52 |
53 | If the optional 'board' parameter is provided, it must be an empty board of
54 | the right size; the same object will be returned.
55 |
56 | Ignores leading and trailing whitespace.
57 |
58 | An ill-formed diagram may give ValueError or a 'best guess'.
59 |
60 | """
61 | if board is None:
62 | board = boards.Board(size)
63 | else:
64 | if board.side != size:
65 | raise ValueError("wrong board size, must be %d" % size)
66 | if not board.is_empty():
67 | raise ValueError("board not empty")
68 | lines = diagram.strip().split("\n")
69 | colours = {'#' : 'b', 'o' : 'w', '.' : None}
70 | if size > 9:
71 | extra_offset = 1
72 | else:
73 | extra_offset = 0
74 | try:
75 | for (row, col) in board.board_points:
76 | colour = colours[lines[size-row-1][3*(col+1)+extra_offset]]
77 | if colour is not None:
78 | board.play(row, col, colour)
79 | except Exception:
80 | raise ValueError
81 | return board
82 |
83 |
84 |
--------------------------------------------------------------------------------
/sgfmill/boards.py:
--------------------------------------------------------------------------------
1 | """Go board representation."""
2 |
3 | from itertools import chain
4 | import functools
5 |
6 | from .common import opponent_of
7 |
8 | class _Group:
9 | """Represent a solidly-connected group.
10 |
11 | Public attributes:
12 | colour
13 | points
14 | is_surrounded
15 |
16 | Points are coordinate pairs (row, col).
17 |
18 | """
19 |
20 | class _Region:
21 | """Represent an empty region.
22 |
23 | Public attributes:
24 | points
25 | neighbouring_colours
26 |
27 | Points are coordinate pairs (row, col).
28 |
29 | """
30 | def __init__(self):
31 | self.points = set()
32 | self.neighbouring_colours = set()
33 |
34 | @functools.lru_cache(maxsize=30)
35 | def _get_all_board_points(side):
36 | return [(_row, _col) for _row in range(side) for _col in range(side)]
37 |
38 | @functools.lru_cache(maxsize=30*30*4)
39 | def _get_neighbours(row, col, side):
40 | neighbours = tuple()
41 | for r, c in [(row-1, col), (row+1, col), (row, col-1), (row, col+1)]:
42 | if (0 <= r < side) and (0 <= c < side):
43 | neighbours += ((r, c),)
44 | return neighbours
45 |
46 | @functools.lru_cache(maxsize=30*30*4)
47 | def _get_neighbours_and_self(row, col, side):
48 | return _get_neighbours(row, col, side) + ((row, col),)
49 |
50 | class Board:
51 | """A legal Go position.
52 |
53 | Supports playing stones with captures, and area scoring.
54 |
55 | Public attributes:
56 | side -- board size (int >= 2)
57 | board_points -- list of coordinates of all points on the board
58 |
59 | """
60 | def __init__(self, side):
61 | self.side = side
62 | if side < 2:
63 | raise ValueError
64 | self._board_points = _get_all_board_points(side)
65 | self.board = []
66 | for row in range(side):
67 | self.board.append([None] * side)
68 | self._is_empty = True
69 |
70 | @property
71 | def board_points(self):
72 | # Returns a copy so that modifying it is safe
73 | return self._board_points[:]
74 |
75 | def copy(self):
76 | """Return an independent copy of this Board."""
77 | b = Board(self.side)
78 | b.board = [self.board[i][:] for i in range(self.side)]
79 | b._is_empty = self._is_empty
80 | return b
81 |
82 | def _make_group(self, row, col, colour):
83 | points = set()
84 | is_surrounded = True
85 | to_handle = set()
86 | to_handle.add((row, col))
87 | while to_handle:
88 | point = to_handle.pop()
89 | points.add(point)
90 | r, c = point
91 | for neighbour in _get_neighbours(r, c, self.side):
92 | (r1, c1) = neighbour
93 | neigh_colour = self.board[r1][c1]
94 | if neigh_colour is None:
95 | is_surrounded = False
96 | elif neigh_colour == colour:
97 | if neighbour not in points:
98 | to_handle.add(neighbour)
99 | group = _Group()
100 | group.colour = colour
101 | group.points = points
102 | group.is_surrounded = is_surrounded
103 | return group
104 |
105 | def _make_empty_region(self, row, col):
106 | points = set()
107 | neighbouring_colours = set()
108 | to_handle = set()
109 | to_handle.add((row, col))
110 | while to_handle:
111 | point = to_handle.pop()
112 | points.add(point)
113 | r, c = point
114 | for neighbour in _get_neighbours(r, c, self.side):
115 | (r1, c1) = neighbour
116 | neigh_colour = self.board[r1][c1]
117 | if neigh_colour is None:
118 | if neighbour not in points:
119 | to_handle.add(neighbour)
120 | else:
121 | neighbouring_colours.add(neigh_colour)
122 | region = _Region()
123 | region.points = points
124 | region.neighbouring_colours = neighbouring_colours
125 | return region
126 |
127 | def _find_surrounded_groups(self, r, c):
128 | """Find solidly-connected groups with 0 liberties adjacent to r,c.
129 |
130 | Returns a list of _Groups.
131 |
132 | """
133 | surrounded = []
134 | handled = set()
135 | for (row, col) in _get_neighbours_and_self(r, c, self.side):
136 | colour = self.board[row][col]
137 | if colour is None:
138 | continue
139 |
140 | point = (row, col)
141 | if point in handled:
142 | continue
143 |
144 | group = self._make_group(row, col, colour)
145 | if group.is_surrounded:
146 | surrounded.append(group)
147 | handled.update(group.points)
148 | return surrounded
149 |
150 | def _find_all_surrounded_groups(self):
151 | """Find all solidly-connected groups with 0 liberties.
152 |
153 | Returns a list of _Groups.
154 |
155 | """
156 | surrounded = []
157 | handled = set()
158 | for (row, col) in self._board_points:
159 | colour = self.board[row][col]
160 | if colour is None:
161 | continue
162 | point = (row, col)
163 | if point in handled:
164 | continue
165 | group = self._make_group(row, col, colour)
166 | if group.is_surrounded:
167 | surrounded.append(group)
168 | handled.update(group.points)
169 | return surrounded
170 |
171 | def is_empty(self):
172 | """Return a boolean indicating whether the board is empty."""
173 | return self._is_empty
174 |
175 | def get(self, row, col):
176 | """Return the state of the specified point.
177 |
178 | Returns a colour, or None for an empty point.
179 |
180 | Raises IndexError if the coordinates are out of range.
181 |
182 | """
183 | if row < 0 or col < 0:
184 | raise IndexError
185 | return self.board[row][col]
186 |
187 | def play(self, row, col, colour):
188 | """Play a move on the board.
189 |
190 | Raises IndexError if the coordinates are out of range.
191 |
192 | Raises ValueError if the specified point isn't empty.
193 |
194 | Performs any necessary captures. Allows self-captures. Doesn't enforce
195 | any ko rule.
196 |
197 | Returns the point forbidden by simple ko, or None
198 |
199 | """
200 | if row < 0 or col < 0:
201 | raise IndexError
202 | opponent = opponent_of(colour)
203 | if self.board[row][col] is not None:
204 | raise ValueError
205 | self.board[row][col] = colour
206 | self._is_empty = False
207 | surrounded = self._find_surrounded_groups(row, col)
208 | simple_ko_point = None
209 | if surrounded:
210 | if len(surrounded) == 1:
211 | to_capture = surrounded
212 | if len(to_capture[0].points) == self.side*self.side:
213 | self._is_empty = True
214 | else:
215 | to_capture = [group for group in surrounded
216 | if group.colour == opponent]
217 | if len(to_capture) == 1 and len(to_capture[0].points) == 1:
218 | self_capture = [group for group in surrounded
219 | if group.colour == colour]
220 | if len(self_capture[0].points) == 1:
221 | (simple_ko_point,) = to_capture[0].points
222 | for group in to_capture:
223 | for r, c in group.points:
224 | self.board[r][c] = None
225 | return simple_ko_point
226 |
227 | def apply_setup(self, black_points, white_points, empty_points):
228 | """Add setup stones or removals to the position.
229 |
230 | This is intended to support SGF AB/AW/AE commands.
231 |
232 | Each parameter is an iterable of coordinate pairs (row, col).
233 |
234 | Applies all the setup specifications, then removes any groups with no
235 | liberties (so the resulting position is always legal).
236 |
237 | If the same point is specified in more than one list, the order in which
238 | they're applied is undefined.
239 |
240 | Returns a boolean saying whether the position was legal as specified.
241 |
242 | Raises IndexError if any coordinates are out of range.
243 |
244 | """
245 | for (row, col) in chain(black_points, white_points, empty_points):
246 | if row < 0 or col < 0 or row >= self.side or col >= self.side:
247 | raise IndexError
248 | for (row, col) in black_points:
249 | self.board[row][col] = 'b'
250 | for (row, col) in white_points:
251 | self.board[row][col] = 'w'
252 | for (row, col) in empty_points:
253 | self.board[row][col] = None
254 | captured = self._find_all_surrounded_groups()
255 | for group in captured:
256 | for row, col in group.points:
257 | self.board[row][col] = None
258 | self._is_empty = True
259 | for (row, col) in self._board_points:
260 | if self.board[row][col] is not None:
261 | self._is_empty = False
262 | break
263 | return not(captured)
264 |
265 | def list_occupied_points(self):
266 | """List all nonempty points.
267 |
268 | Returns a list of pairs (colour, (row, col))
269 |
270 | """
271 | result = []
272 | for (row, col) in self._board_points:
273 | colour = self.board[row][col]
274 | if colour is not None:
275 | result.append((colour, (row, col)))
276 | return result
277 |
278 | def area_score(self):
279 | """Calculate the area score of a position.
280 |
281 | Assumes all stones are alive.
282 |
283 | Returns black score minus white score.
284 |
285 | Doesn't take komi into account.
286 |
287 | """
288 | scores = {'b' : 0, 'w' : 0}
289 | handled = set()
290 | for (row, col) in self._board_points:
291 | colour = self.board[row][col]
292 | if colour is not None:
293 | scores[colour] += 1
294 | continue
295 | point = (row, col)
296 | if point in handled:
297 | continue
298 | region = self._make_empty_region(row, col)
299 | region_size = len(region.points)
300 | for colour in ('b', 'w'):
301 | if colour in region.neighbouring_colours:
302 | scores[colour] += region_size
303 | handled.update(region.points)
304 | return scores['b'] - scores['w']
305 |
306 |
--------------------------------------------------------------------------------
/sgfmill/common.py:
--------------------------------------------------------------------------------
1 | """Domain-dependent utility functions for sgfmill.
2 |
3 | This module is designed to be used with 'from common import *'.
4 |
5 | This is for Go-specific utilities.
6 |
7 | """
8 |
9 | __all__ = ["opponent_of", "colour_name", "format_vertex", "format_vertex_list",
10 | "move_from_vertex"]
11 |
12 | _opponents = {"b":"w", "w":"b"}
13 | def opponent_of(colour):
14 | """Return the opponent colour.
15 |
16 | colour -- 'b' or 'w'
17 |
18 | Returns 'b' or 'w'.
19 |
20 | """
21 | try:
22 | return _opponents[colour]
23 | except KeyError:
24 | raise ValueError from None
25 |
26 | def colour_name(colour):
27 | """Return the (lower-case) full name of a colour.
28 |
29 | colour -- 'b' or 'w'
30 |
31 | """
32 | try:
33 | return {'b': 'black', 'w': 'white'}[colour]
34 | except KeyError:
35 | raise ValueError from None
36 |
37 |
38 | column_letters = "ABCDEFGHJKLMNOPQRSTUVWXYZ"
39 |
40 | def format_vertex(move):
41 | """Return coordinates as a string like 'A1', or 'pass'.
42 |
43 | move -- pair (row, col), or None for a pass
44 |
45 | The result is suitable for use directly in GTP responses.
46 |
47 | """
48 | if move is None:
49 | return "pass"
50 | row, col = move
51 | if not 0 <= row < 25 or not 0 <= col < 25:
52 | raise ValueError
53 | return column_letters[col] + str(row+1)
54 |
55 | def format_vertex_list(moves):
56 | """Return a list of coordinates as a string like 'A1,B2'."""
57 | return ",".join(map(format_vertex, moves))
58 |
59 | def move_from_vertex(vertex, board_size):
60 | """Interpret a string representing a vertex, as specified by GTP.
61 |
62 | Returns a pair of coordinates (row, col) in range(0, board_size)
63 |
64 | Raises ValueError with an appropriate message if 'vertex' isn't a valid GTP
65 | vertex specification for a board of size 'board_size'.
66 |
67 | """
68 | if not 0 < board_size <= 25:
69 | raise ValueError("board_size out of range")
70 | try:
71 | s = vertex.lower()
72 | except Exception:
73 | raise ValueError("invalid vertex") from None
74 | if s == "pass":
75 | return None
76 | try:
77 | col_c = s[0]
78 | if (not "a" <= col_c <= "z") or col_c == "i":
79 | raise ValueError
80 | if col_c > "i":
81 | col = ord(col_c) - ord("b")
82 | else:
83 | col = ord(col_c) - ord("a")
84 | row = int(s[1:]) - 1
85 | if row < 0:
86 | raise ValueError
87 | except (IndexError, ValueError):
88 | raise ValueError("invalid vertex: '%s'" % s) from None
89 | if not (col < board_size and row < board_size):
90 | raise ValueError("vertex is off board: '%s'" % s)
91 | return row, col
92 |
93 |
--------------------------------------------------------------------------------
/sgfmill/sgf_board_interface.py:
--------------------------------------------------------------------------------
1 | """Description of the board interfaces required by sgf_moves."""
2 |
3 | class Interface_for_get_setup_and_moves:
4 | """Interface required by sgf_moves.get_setup_and_moves().
5 |
6 | Required public attributes:
7 | side -- board size (int >= 2)
8 |
9 | """
10 |
11 | def is_empty(self):
12 | """Return a boolean indicating whether the board is empty."""
13 | raise NotImplementedError
14 |
15 | def apply_setup(self, black_points, white_points, empty_points):
16 | """Add setup stones or removals to the position.
17 |
18 | See boards.Board.apply_setup() for details.
19 |
20 | """
21 | raise NotImplementedError
22 |
23 | class Interface_for_set_initial_position:
24 | """Interface required by sgf_moves.set_initial_position()."""
25 |
26 | def list_occupied_points(self):
27 | """List all nonempty points.
28 |
29 | Returns a list of pairs (colour, (row, col))
30 |
31 | """
32 | raise NotImplementedError
33 |
--------------------------------------------------------------------------------
/sgfmill/sgf_grammar.py:
--------------------------------------------------------------------------------
1 | """Parse and serialise SGF data.
2 |
3 | This is intended for use with SGF FF[4]; see http://www.red-bean.com/sgf/
4 |
5 | Nothing in this module is Go-specific.
6 |
7 | This module is encoding-agnostic: it works with bytes-like objects representing
8 | 8-bit strings in an arbitrary 'ascii-compatible' encoding.
9 |
10 |
11 | In the documentation below, a _property map_ is a dict mapping a PropIdent to a
12 | nonempty list of raw property values.
13 |
14 | A raw property value is a bytes object containing a PropValue without its
15 | enclosing brackets, but with backslashes and line endings left untouched.
16 |
17 | So a property map's keys should pass is_valid_property_identifier(), and its
18 | values should pass is_valid_property_value().
19 |
20 | """
21 |
22 | import re
23 | import string
24 |
25 |
26 | _propident_re = re.compile(r"\A[A-Z]{1,64}\Z")
27 | _propvalue_re = re.compile(br"\A [^\\\]]* (?: \\. [^\\\]]* )* \Z",
28 | re.VERBOSE | re.DOTALL)
29 | _find_start_re = re.compile(br"\(\s*;")
30 | _tokenise_re = re.compile(br"""
31 | \s*
32 | (?:
33 | \[ (?P [^\\\]]* (?: \\. [^\\\]]* )* ) \] # PropValue
34 | |
35 | (?P [A-Za-z]{1,64} ) # PropIdent (accepting lc)
36 | |
37 | (?P [;()] ) # delimiter
38 | )
39 | """, re.VERBOSE | re.DOTALL)
40 |
41 |
42 | def is_valid_property_identifier(s):
43 | """Check whether 's' is a well-formed PropIdent.
44 |
45 | s -- string (_not_ a bytes object).
46 |
47 | Details:
48 | - it doesn't permit lower-case letters
49 | - it accepts at most 64 letters (there is no limit in the spec; no
50 | standard property has more than 2; a report from 2017-04 says the
51 | longest found in the wild is "MULTIGOGM")
52 |
53 | This accepts the same values as the tokeniser, except that the tokeniser
54 | does permit lower-case letters.
55 |
56 | """
57 | return bool(_propident_re.search(s))
58 |
59 | def is_valid_property_value(bb):
60 | """Check whether 'bb' is a well-formed PropValue.
61 |
62 | bb -- bytes-like object
63 |
64 | This accepts the same values as the tokeniser: any string that doesn't
65 | contain an unescaped ] or end with an unescaped \ .
66 |
67 | """
68 | return bool(_propvalue_re.search(bb))
69 |
70 |
71 | _lcbytes = string.ascii_lowercase.encode()
72 |
73 | def tokenise(bb, start_position=0):
74 | """Tokenise a string containing SGF data.
75 |
76 | bb -- bytes-like object
77 | start_position -- index into 'bb'
78 |
79 | Skips leading junk.
80 |
81 | Returns a list of pairs (token type : str, contents : bytes),
82 | and also the index in 'bb' of the start of the unprocessed 'tail'.
83 |
84 | token types and contents:
85 | I -- PropIdent: upper-case letters
86 | V -- PropValue: raw value, without the enclosing brackets
87 | D -- delimiter: ';', '(', or ')'
88 |
89 | Stops when it has seen as many closing parens as open ones, at the end of
90 | the string, or when it first finds something it can't tokenise.
91 |
92 | The first two tokens are always '(' and ';' (otherwise it won't find the
93 | start of the content).
94 |
95 | Accepts lower-case letters in PropIdents (these were allowed in some
96 | ancient SGF variants, and are still seen in the wild); the returned
97 | PropIdent has the lower-case letters removed (for example, 'AddBlack' is
98 | returned as 'AB'), and therefore passes is_valid_property_identifier().
99 |
100 | """
101 | result = []
102 | m = _find_start_re.search(bb, start_position)
103 | if not m:
104 | return [], 0
105 | i = m.start()
106 | depth = 0
107 | while True:
108 | m = _tokenise_re.match(bb, i)
109 | if not m:
110 | break
111 | group = m.lastgroup
112 | token = m.group(m.lastindex)
113 | if group == 'I':
114 | token = token.translate(None, _lcbytes)
115 | result.append((group, token))
116 | i = m.end()
117 | if group == 'D':
118 | if token == b'(':
119 | depth += 1
120 | elif token == b')':
121 | depth -= 1
122 | if depth == 0:
123 | break
124 | return result, i
125 |
126 | class Coarse_game_tree:
127 | """An SGF GameTree.
128 |
129 | This is a direct representation of the SGF parse tree. It's 'coarse' in the
130 | sense that the objects in the tree structure represent node sequences, not
131 | individual nodes.
132 |
133 | Public attributes
134 | sequence -- nonempty list of property maps
135 | children -- list of Coarse_game_trees
136 |
137 | The sequence represents the nodes before the variations.
138 |
139 | """
140 | def __init__(self):
141 | self.sequence = [] # must be at least one node
142 | self.children = [] # may be empty
143 |
144 | def _parse_sgf_game(bb, start_position):
145 | """Common implementation for parse_sgf_game and parse_sgf_games."""
146 | tokens, end_position = tokenise(bb, start_position)
147 | if not tokens:
148 | return None, None
149 | stack = []
150 | game_tree = None
151 | sequence = None
152 | properties = None
153 | index = 0
154 | try:
155 | while True:
156 | token_type, token = tokens[index]
157 | index += 1
158 | if token_type == 'V':
159 | raise ValueError("unexpected value")
160 | if token_type == 'D':
161 | if token == b';':
162 | if sequence is None:
163 | raise ValueError("unexpected node")
164 | properties = {}
165 | sequence.append(properties)
166 | else:
167 | if sequence is not None:
168 | if not sequence:
169 | raise ValueError("empty sequence")
170 | game_tree.sequence = sequence
171 | sequence = None
172 | if token == b'(':
173 | stack.append(game_tree)
174 | game_tree = Coarse_game_tree()
175 | sequence = []
176 | else:
177 | # token == b')'
178 | variation = game_tree
179 | game_tree = stack.pop()
180 | if game_tree is None:
181 | break
182 | game_tree.children.append(variation)
183 | properties = None
184 | else:
185 | # token_type == 'I'
186 | prop_ident = token.decode("ascii")
187 | prop_values = []
188 | while True:
189 | token_type, token = tokens[index]
190 | if token_type != 'V':
191 | break
192 | index += 1
193 | prop_values.append(token)
194 | if not prop_values:
195 | raise ValueError("property with no values")
196 | try:
197 | if prop_ident in properties:
198 | properties[prop_ident] += prop_values
199 | else:
200 | properties[prop_ident] = prop_values
201 | except TypeError:
202 | raise ValueError("property value outside a node") from None
203 | except IndexError:
204 | raise ValueError("unexpected end of SGF data") from None
205 | assert index == len(tokens)
206 | return variation, end_position
207 |
208 | def parse_sgf_game(bb):
209 | """Read a single SGF game from bytes data, returning the parse tree.
210 |
211 | bb -- bytes-like object
212 |
213 | Returns a Coarse_game_tree.
214 |
215 | Applies the rules for FF[4].
216 |
217 | Raises ValueError if can't parse the data.
218 |
219 | If a property appears more than once in a node (which is not permitted by
220 | the spec), treats it the same as a single property with multiple values.
221 |
222 |
223 | Identifies the start of the SGF content by looking for '(;' (with possible
224 | whitespace between); ignores everything preceding that. Ignores everything
225 | following the first game.
226 |
227 | """
228 | game_tree, _ = _parse_sgf_game(bb, 0)
229 | if game_tree is None:
230 | raise ValueError("no SGF data found")
231 | return game_tree
232 |
233 | def parse_sgf_collection(bb):
234 | """Read an SGF game collection, returning the parse trees.
235 |
236 | bb -- bytes-like object
237 |
238 | Returns a nonempty list of Coarse_game_trees.
239 |
240 | Raises ValueError if no games were found in the data.
241 |
242 | Raises ValueError if there is an error parsing a game. See
243 | parse_sgf_game() for details.
244 |
245 |
246 | Ignores non-SGF data before the first game, between games, and after the
247 | final game. Identifies the start of each game in the same way as
248 | parse_sgf_game().
249 |
250 | """
251 | position = 0
252 | result = []
253 | while True:
254 | try:
255 | game_tree, position = _parse_sgf_game(bb, position)
256 | except ValueError as e:
257 | raise ValueError("error parsing game %d: %s" % (len(result), e)) \
258 | from None
259 | if game_tree is None:
260 | break
261 | result.append(game_tree)
262 | if not result:
263 | raise ValueError("no SGF data found")
264 | return result
265 |
266 |
267 | def block_format(pieces, width=79):
268 | """Concatenate strings, adding newlines.
269 |
270 | pieces -- iterable of bytes-like objects
271 | width -- int (default 79)
272 |
273 | Returns b"".join(pieces), with added newlines between pieces as necessary
274 | to avoid lines longer than 'width' (using nothing more sophisticated than a
275 | byte-count).
276 |
277 | Leaves newlines inside 'pieces' untouched, and ignores them in its width
278 | calculation. If a single piece is longer than 'width', it will become a
279 | single long line in the output.
280 |
281 | """
282 | lines = []
283 | line = b""
284 | for bb in pieces:
285 | if len(line) + len(bb) > width:
286 | lines.append(line)
287 | line = b""
288 | line += bb
289 | if line:
290 | lines.append(line)
291 | return b"\n".join(lines)
292 |
293 | def serialise_game_tree(game_tree, wrap=79):
294 | """Serialise an SGF game as a string.
295 |
296 | game_tree -- Coarse_game_tree
297 | wrap -- int (default 79), or None
298 |
299 | Returns a bytes object, ending with a newline.
300 |
301 | If 'wrap' is not None, makes some effort to keep output lines no longer
302 | than 'wrap'.
303 |
304 | """
305 | l = []
306 | to_serialise = [game_tree]
307 | while to_serialise:
308 | game_tree = to_serialise.pop()
309 | if game_tree is None:
310 | l.append(b")")
311 | continue
312 | l.append(b"(")
313 | for properties in game_tree.sequence:
314 | l.append(b";")
315 | # Force FF to the front, largely to work around a Quarry bug which
316 | # makes it ignore the first few bytes of the file.
317 | for prop_ident, prop_values in sorted(
318 | properties.items(),
319 | key=lambda kv: (-(kv[0]=="FF"), kv[0])):
320 | # Make a single string for each property, to get prettier
321 | # block_format output.
322 | m = [prop_ident.encode("ascii")]
323 | for value in prop_values:
324 | m.append(b"[" + value + b"]")
325 | l.append(b"".join(m))
326 | to_serialise.append(None)
327 | to_serialise.extend(reversed(game_tree.children))
328 | l.append(b"\n")
329 | if wrap is None:
330 | return b"".join(l)
331 | else:
332 | return block_format(l, wrap)
333 |
334 |
335 | def make_tree(game_tree, root, node_builder, node_adder):
336 | """Construct a node tree from a Coarse_game_tree.
337 |
338 | game_tree -- Coarse_game_tree
339 | root -- node
340 | node_builder -- function taking parameters (parent node, property map)
341 | returning a node
342 | node_adder -- function taking a pair (parent node, child node)
343 |
344 | Builds a tree of nodes corresponding to this GameTree, calling
345 | node_builder() to make new nodes and node_adder() to add child nodes to
346 | their parent.
347 |
348 | Makes no further assumptions about the node type.
349 |
350 | """
351 | to_build = [(root, game_tree, 0)]
352 | while to_build:
353 | node, game_tree, index = to_build.pop()
354 | if index < len(game_tree.sequence) - 1:
355 | child = node_builder(node, game_tree.sequence[index+1])
356 | node_adder(node, child)
357 | to_build.append((child, game_tree, index+1))
358 | else:
359 | node._children = []
360 | for child_tree in game_tree.children:
361 | child = node_builder(node, child_tree.sequence[0])
362 | node_adder(node, child)
363 | to_build.append((child, child_tree, 0))
364 |
365 | def make_coarse_game_tree(root, get_children, get_properties):
366 | """Construct a Coarse_game_tree from a node tree.
367 |
368 | root -- node
369 | get_children -- function taking a node, returning a sequence of nodes
370 | get_properties -- function taking a node, returning a property map
371 |
372 | Returns a Coarse_game_tree.
373 |
374 | Walks the node tree based at 'root' using get_children(), and uses
375 | get_properties() to extract the raw properties.
376 |
377 | Makes no further assumptions about the node type.
378 |
379 | Doesn't check that the property maps have well-formed keys and values.
380 |
381 | """
382 | result = Coarse_game_tree()
383 | to_serialise = [(result, root)]
384 | while to_serialise:
385 | game_tree, node = to_serialise.pop()
386 | while True:
387 | game_tree.sequence.append(get_properties(node))
388 | children = get_children(node)
389 | if len(children) != 1:
390 | break
391 | node = children[0]
392 | for child in children:
393 | child_tree = Coarse_game_tree()
394 | game_tree.children.append(child_tree)
395 | to_serialise.append((child_tree, child))
396 | return result
397 |
398 |
399 | def main_sequence_iter(game_tree):
400 | """Provide the 'leftmost' complete sequence of a Coarse_game_tree.
401 |
402 | game_tree -- Coarse_game_tree
403 |
404 | Returns an iterable of property maps.
405 |
406 | If the game has no variations, this provides the complete game. Otherwise,
407 | it chooses the first variation each time it has a choice.
408 |
409 | """
410 | while True:
411 | for properties in game_tree.sequence:
412 | yield properties
413 | if not game_tree.children:
414 | break
415 | game_tree = game_tree.children[0]
416 |
417 |
418 | _split_compose_re = re.compile(
419 | br"( (?: [^\\:] | \\. )* ) :",
420 | re.VERBOSE | re.DOTALL)
421 |
422 | def parse_compose(bb):
423 | """Split the parts of an SGF Compose value.
424 |
425 | If the value is a well-formed Compose, returns a pair of strings.
426 |
427 | If it isn't (ie, there is no delimiter), returns the complete string and
428 | None.
429 |
430 | Interprets backslash escapes in order to find the delimiter, but leaves
431 | backslash escapes unchanged in the returned strings.
432 |
433 | """
434 | m = _split_compose_re.match(bb)
435 | if not m:
436 | return bb, None
437 | return m.group(1), bb[m.end():]
438 |
439 | def compose(bb1, bb2):
440 | """Construct a value of Compose value type.
441 |
442 | bb1, bb2 -- serialised form of a property value (bytes-like objects)
443 |
444 | (This is only needed if the type of the first value permits colons.)
445 |
446 | """
447 | return bb1.replace(b":", b"\\:") + b":" + bb2
448 |
449 |
450 | _newline_re = re.compile(br"\n\r|\r\n|\n|\r")
451 | _whitespace_table = bytes.maketrans(b"\t\f\v", b" ")
452 | _chunk_re = re.compile(br" [^\n\\]+ | [\n\\] ", re.VERBOSE)
453 |
454 | def simpletext_value(bb):
455 | """Convert a raw SimpleText property value to the bytestring it represents.
456 |
457 | bb -- bytes-like object
458 |
459 | Returns a bytes object, in the same encoding as 'bb'.
460 |
461 | This interprets escape characters, and does whitespace mapping:
462 |
463 | - backslash followed by linebreak (LF, CR, LFCR, or CRLF) disappears
464 | - any other linebreak is replaced by a space
465 | - any other whitespace character is replaced by a space
466 | - other backslashes disappear (but double-backslash -> single-backslash)
467 |
468 | """
469 | bb = _newline_re.sub(b"\n", bb)
470 | bb = bb.translate(_whitespace_table)
471 | is_escaped = False
472 | result = []
473 | for chunk in _chunk_re.findall(bb):
474 | if is_escaped:
475 | if chunk != b"\n":
476 | result.append(chunk)
477 | is_escaped = False
478 | elif chunk == b"\\":
479 | is_escaped = True
480 | elif chunk == b"\n":
481 | result.append(b" ")
482 | else:
483 | result.append(chunk)
484 | return b"".join(result)
485 |
486 | def text_value(bb):
487 | """Convert a raw Text property value to the bytestring it represents.
488 |
489 | bb -- bytes-like object
490 |
491 | Returns a bytes object, in the same encoding as 'bb'.
492 |
493 | This interprets escape characters, and does whitespace mapping:
494 |
495 | - linebreak (LF, CR, LFCR, or CRLF) is converted to \n
496 | - any other whitespace character is replaced by a space
497 | - backslash followed by linebreak disappears
498 | - other backslashes disappear (but double-backslash -> single-backslash)
499 |
500 | """
501 | bb = _newline_re.sub(b"\n", bb)
502 | bb = bb.translate(_whitespace_table)
503 | is_escaped = False
504 | result = []
505 | for chunk in _chunk_re.findall(bb):
506 | if is_escaped:
507 | if chunk != b"\n":
508 | result.append(chunk)
509 | is_escaped = False
510 | elif chunk == b"\\":
511 | is_escaped = True
512 | else:
513 | result.append(chunk)
514 | return b"".join(result)
515 |
516 | def escape_text(bb):
517 | """Convert a bytestring to a raw Text property value that represents it.
518 |
519 | bb -- bytes-like object, in the desired output encoding.
520 |
521 | Returns a bytes object which passes is_valid_property_value().
522 |
523 | Normally text_value(escape_text(bb)) == bb, but there are the following
524 | exceptions:
525 | - all linebreaks are are normalised to \n
526 | - whitespace other than line breaks is converted to a single space
527 |
528 | """
529 | return bb.replace(b"\\", b"\\\\").replace(b"]", b"\\]")
530 |
531 |
--------------------------------------------------------------------------------
/sgfmill/sgf_moves.py:
--------------------------------------------------------------------------------
1 | """Higher-level processing of moves and positions from SGF games."""
2 |
3 | from . import sgf_properties
4 |
5 | def get_setup_and_moves(sgf_game, board=None):
6 | """Return the initial setup and the following moves from an Sgf_game.
7 |
8 | Returns a pair (board, plays)
9 |
10 | board -- boards.Board
11 | plays -- list of pairs (colour, move)
12 | moves are (row, col), or None for a pass.
13 |
14 | The board represents the position described by AB and/or AW properties
15 | in the root node.
16 |
17 | The moves are from the game's 'leftmost' variation.
18 |
19 | Raises ValueError if this position isn't legal.
20 |
21 | Raises ValueError if there are any AB/AW/AE properties after the root
22 | node.
23 |
24 | Doesn't check whether the moves are legal.
25 |
26 | If the optional 'board' parameter is provided, it must be an empty board of
27 | the right size; the same object will be returned.
28 | See sgf_board_interface.Interface_for_get_setup_and_moves
29 | for the interface it should support.
30 |
31 | """
32 | size = sgf_game.get_size()
33 | if board is None:
34 | from . import boards
35 | board = boards.Board(size)
36 | else:
37 | if board.side != size:
38 | raise ValueError("wrong board size, must be %d" % size)
39 | if not board.is_empty():
40 | raise ValueError("board not empty")
41 | root = sgf_game.get_root()
42 | nodes = sgf_game.main_sequence_iter()
43 | ab, aw, ae = root.get_setup_stones()
44 | if ab or aw:
45 | is_legal = board.apply_setup(ab, aw, ae)
46 | if not is_legal:
47 | raise ValueError("setup position not legal")
48 | colour, raw = root.get_raw_move()
49 | if colour is not None:
50 | raise ValueError("mixed setup and moves in root node")
51 | next(nodes)
52 | moves = []
53 | for node in nodes:
54 | if node.has_setup_stones():
55 | raise ValueError("setup properties after the root node")
56 | colour, raw = node.get_raw_move()
57 | if colour is not None:
58 | moves.append((colour, sgf_properties.interpret_go_point(raw, size)))
59 | return board, moves
60 |
61 | def set_initial_position(sgf_game, board):
62 | """Add setup stones to an Sgf_game reflecting a board position.
63 |
64 | sgf_game -- Sgf_game
65 | board -- Board object
66 |
67 | Replaces any existing setup stones in the Sgf_game's root node.
68 |
69 | 'board' may be a boards.Board instance, or anything providing the interface
70 | described by sgf_board_interface.Interface_for_set_initial_position().
71 |
72 | """
73 | stones = {'b' : set(), 'w' : set()}
74 | for (colour, point) in board.list_occupied_points():
75 | stones[colour].add(point)
76 | sgf_game.get_root().set_setup_stones(stones['b'], stones['w'])
77 |
78 | def indicate_first_player(sgf_game):
79 | """Add a PL property to the root node if appropriate.
80 |
81 | Looks at the first child of the root to see who the first player is, and
82 | sets PL it isn't the expected player (ie, black normally, but white if
83 | there is a handicap), or if there are non-handicap setup stones.
84 |
85 | """
86 | root = sgf_game.get_root()
87 | first_player, move = root[0].get_move()
88 | if first_player is None:
89 | return
90 | has_handicap = root.has_property("HA")
91 | if root.has_property("AW"):
92 | specify_pl = True
93 | elif root.has_property("AB") and not has_handicap:
94 | specify_pl = True
95 | elif not has_handicap and first_player == 'w':
96 | specify_pl = True
97 | elif has_handicap and first_player == 'b':
98 | specify_pl = True
99 | else:
100 | specify_pl = False
101 | if specify_pl:
102 | root.set('PL', first_player)
103 |
104 |
--------------------------------------------------------------------------------
/sgfmill_tests/__init__.py:
--------------------------------------------------------------------------------
1 | # sgfmill_tests package
2 |
--------------------------------------------------------------------------------
/sgfmill_tests/board_test_data.py:
--------------------------------------------------------------------------------
1 | play_tests = [
2 |
3 | # code, list of moves to play, board representation, simple ko point, score
4 |
5 | ('blank', [
6 | ], """\
7 | 9 . . . . . . . . .
8 | 8 . . . . . . . . .
9 | 7 . . . . . . . . .
10 | 6 . . . . . . . . .
11 | 5 . . . . . . . . .
12 | 4 . . . . . . . . .
13 | 3 . . . . . . . . .
14 | 2 . . . . . . . . .
15 | 1 . . . . . . . . .
16 | A B C D E F G H J
17 | """, None, 0),
18 |
19 | ('twostone', [
20 | "B B2", "W C2",
21 | ], """\
22 | 9 . . . . . . . . .
23 | 8 . . . . . . . . .
24 | 7 . . . . . . . . .
25 | 6 . . . . . . . . .
26 | 5 . . . . . . . . .
27 | 4 . . . . . . . . .
28 | 3 . . . . . . . . .
29 | 2 . # o . . . . . .
30 | 1 . . . . . . . . .
31 | A B C D E F G H J
32 | """, None, 0),
33 |
34 | ('many-groups-1-capture', [
35 | "B C3", "W D3", "B C5", "B C4", "W D4", "B H1", "B B9", "B J6",
36 | "B A7", "B B7", "W A3", "W J2", "W H2", "W G2", "W J3",
37 | "B F7",
38 | "W E6", "W G8", "W G6", "W F8", "W E7", "W F6", "W G7", "W E8",
39 | ], """\
40 | 9 . # . . . . . . .
41 | 8 . . . . o o o . .
42 | 7 # # . . o . o . .
43 | 6 . . . . o o o . #
44 | 5 . . # . . . . . .
45 | 4 . . # o . . . . .
46 | 3 o . # o . . . . o
47 | 2 . . . . . . o o o
48 | 1 . . . . . . . # .
49 | A B C D E F G H J
50 | """, None, -8),
51 |
52 | ('corner-bl', [
53 | "B A1", "W B1", "W A2",
54 | ], """\
55 | 9 . . . . . . . . .
56 | 8 . . . . . . . . .
57 | 7 . . . . . . . . .
58 | 6 . . . . . . . . .
59 | 5 . . . . . . . . .
60 | 4 . . . . . . . . .
61 | 3 . . . . . . . . .
62 | 2 o . . . . . . . .
63 | 1 . o . . . . . . .
64 | A B C D E F G H J
65 | """, None, -81),
66 |
67 | ('corner-all', [
68 | "B A1", "W B1", "W A2",
69 | "B J1", "W H1", "W J2",
70 | "B A9", "W B9", "W A8",
71 | "B J9", "W H9", "W J8",
72 | ], """\
73 | 9 . o . . . . . o .
74 | 8 o . . . . . . . o
75 | 7 . . . . . . . . .
76 | 6 . . . . . . . . .
77 | 5 . . . . . . . . .
78 | 4 . . . . . . . . .
79 | 3 . . . . . . . . .
80 | 2 o . . . . . . . o
81 | 1 . o . . . . . o .
82 | A B C D E F G H J
83 | """, None, -81),
84 |
85 | ('multiple', [
86 | "W D4", "B D3", "W C5", "B C4", "W E5", "B E4",
87 | "B B5", "B F5", "B C6", "B E6", "B D7", "W D6",
88 | "B D5",
89 | ], """\
90 | 9 . . . . . . . . .
91 | 8 . . . . . . . . .
92 | 7 . . . # . . . . .
93 | 6 . . # . # . . . .
94 | 5 . # . # . # . . .
95 | 4 . . # . # . . . .
96 | 3 . . . # . . . . .
97 | 2 . . . . . . . . .
98 | 1 . . . . . . . . .
99 | A B C D E F G H J
100 | """, None, 81),
101 |
102 | ('large', [
103 | "W D2", "W G2", "W E3", "W F3", "W F4", "W D5", "W E5", "W F5", "B E2", "B F2",
104 | "B D3", "B G3", "B D4", "B G4", "B C5", "B G5", "B D6", "B E6", "B F6", "B E4",
105 | ], """\
106 | 9 . . . . . . . . .
107 | 8 . . . . . . . . .
108 | 7 . . . . . . . . .
109 | 6 . . . # # # . . .
110 | 5 . . # . . . # . .
111 | 4 . . . # # . # . .
112 | 3 . . . # . . # . .
113 | 2 . . . o # # o . .
114 | 1 . . . . . . . . .
115 | A B C D E F G H J
116 | """, None, 16),
117 |
118 | ('pre-recapture', [
119 | "W A1", "W B1", "W B2", "W C2", "W D3", "W E3", "W A4", "W B4", "W C4", "W E4",
120 | "B A2", "B D2", "B E2", "B A3", "B B3", "B F3", "B D4", "B F4", "B E5",
121 | "B C3",
122 | ], """\
123 | 9 . . . . . . . . .
124 | 8 . . . . . . . . .
125 | 7 . . . . . . . . .
126 | 6 . . . . . . . . .
127 | 5 . . . . # . . . .
128 | 4 o o o # . # . . .
129 | 3 # # # . . # . . .
130 | 2 # o o # # . . . .
131 | 1 o o . . . . . . .
132 | A B C D E F G H J
133 | """, None, 6),
134 |
135 | ('recapture', [
136 | "W A1", "W B1", "W B2", "W C2", "W D3", "W E3", "W A4", "W B4", "W C4", "W E4",
137 | "B A2", "B D2", "B E2", "B A3", "B B3", "B F3", "B D4", "B F4", "B E5",
138 | "B C3", "W D3",
139 | ], """\
140 | 9 . . . . . . . . .
141 | 8 . . . . . . . . .
142 | 7 . . . . . . . . .
143 | 6 . . . . . . . . .
144 | 5 . . . . # . . . .
145 | 4 o o o # . # . . .
146 | 3 . . . o . # . . .
147 | 2 . o o # # . . . .
148 | 1 o o . . . . . . .
149 | A B C D E F G H J
150 | """, None, -6),
151 |
152 | ('self-capture-1', [
153 | "B D4", "B C5", "B E5", "B D6", "W D5",
154 | ], """\
155 | 9 . . . . . . . . .
156 | 8 . . . . . . . . .
157 | 7 . . . . . . . . .
158 | 6 . . . # . . . . .
159 | 5 . . # . # . . . .
160 | 4 . . . # . . . . .
161 | 3 . . . . . . . . .
162 | 2 . . . . . . . . .
163 | 1 . . . . . . . . .
164 | A B C D E F G H J
165 | """, None, 81),
166 |
167 | ('self-capture-2', [
168 | "B D4", "B E4", "B C5", "B F5", "B D6", "B E6", "W D5", "W E5",
169 | ], """\
170 | 9 . . . . . . . . .
171 | 8 . . . . . . . . .
172 | 7 . . . . . . . . .
173 | 6 . . . # # . . . .
174 | 5 . . # . . # . . .
175 | 4 . . . # # . . . .
176 | 3 . . . . . . . . .
177 | 2 . . . . . . . . .
178 | 1 . . . . . . . . .
179 | A B C D E F G H J
180 | """, None, 81),
181 |
182 | ('self-capture-3', [
183 | "B D4", "B E4", "B F4", "B C5", "B G5", "B D6", "B E6", "B F6",
184 | "W D5", "W F5", "W E5",
185 | ], """\
186 | 9 . . . . . . . . .
187 | 8 . . . . . . . . .
188 | 7 . . . . . . . . .
189 | 6 . . . # # # . . .
190 | 5 . . # . . . # . .
191 | 4 . . . # # # . . .
192 | 3 . . . . . . . . .
193 | 2 . . . . . . . . .
194 | 1 . . . . . . . . .
195 | A B C D E F G H J
196 | """, None, 81),
197 |
198 | ('self-capture-many', [
199 | "B E2", "B D3", "B F3", "B D4", "B F4", "B G4", "B C5", "B H5", "B C6",
200 | "B F6", "B G6", "B D7", "B E7",
201 | "W E3", "W E4", "W D5", "W F5", "W G5", "W D6", "W E6", "W E5",
202 | ], """\
203 | 9 . . . . . . . . .
204 | 8 . . . . . . . . .
205 | 7 . . . # # . . . .
206 | 6 . . # . . # # . .
207 | 5 . . # . . . . # .
208 | 4 . . . # . # # . .
209 | 3 . . . # . # . . .
210 | 2 . . . . # . . . .
211 | 1 . . . . . . . . .
212 | A B C D E F G H J
213 | """, None, 81),
214 |
215 | ('ko-corner', [
216 | "B A1", "B B2", "B A3",
217 | "W B1", "W A2",
218 | ], """\
219 | 9 . . . . . . . . .
220 | 8 . . . . . . . . .
221 | 7 . . . . . . . . .
222 | 6 . . . . . . . . .
223 | 5 . . . . . . . . .
224 | 4 . . . . . . . . .
225 | 3 # . . . . . . . .
226 | 2 o # . . . . . . .
227 | 1 . o . . . . . . .
228 | A B C D E F G H J
229 | """, 'A1', -1),
230 |
231 | ('notko-twocaptured', [
232 | "B B2", "B B3", "B A4",
233 | "W B1", "W A2", "W A3",
234 | "B A1",
235 | ], """\
236 | 9 . . . . . . . . .
237 | 8 . . . . . . . . .
238 | 7 . . . . . . . . .
239 | 6 . . . . . . . . .
240 | 5 . . . . . . . . .
241 | 4 # . . . . . . . .
242 | 3 . # . . . . . . .
243 | 2 . # . . . . . . .
244 | 1 # o . . . . . . .
245 | A B C D E F G H J
246 | """, None, 5),
247 |
248 | ('notko-tworecaptured', [
249 | "B A1", "B B3", "B A4",
250 | "W B1", "W B2", "W A3",
251 | "B A2",
252 | ], """\
253 | 9 . . . . . . . . .
254 | 8 . . . . . . . . .
255 | 7 . . . . . . . . .
256 | 6 . . . . . . . . .
257 | 5 . . . . . . . . .
258 | 4 # . . . . . . . .
259 | 3 . # . . . . . . .
260 | 2 # o . . . . . . .
261 | 1 # o . . . . . . .
262 | A B C D E F G H J
263 | """, None, 3),
264 |
265 | ]
266 |
267 |
268 | score_tests = [
269 |
270 | # code, board representation, score
271 |
272 | ('empty', """\
273 | 9 . . . . . . . . .
274 | 8 . . . . . . . . .
275 | 7 . . . . . . . . .
276 | 6 . . . . . . . . .
277 | 5 . . . . . . . . .
278 | 4 . . . . . . . . .
279 | 3 . . . . . . . . .
280 | 2 . . . . . . . . .
281 | 1 . . . . . . . . .
282 | A B C D E F G H J
283 | """, 0),
284 |
285 | ('onestone', """\
286 | 9 . . . . . . . . .
287 | 8 . . . . . . . . .
288 | 7 . . . . . . . . .
289 | 6 . . . . . . . . .
290 | 5 . . . . . . . . .
291 | 4 . . . . . . . . .
292 | 3 . . . . . . . . .
293 | 2 . # . . . . . . .
294 | 1 . . . . . . . . .
295 | A B C D E F G H J
296 | """, 81),
297 |
298 | ('easy', """\
299 | 9 . . . # o . . . .
300 | 8 . . . # o . . . .
301 | 7 . . . # o . . . .
302 | 6 . . . # o . . . .
303 | 5 . . . # o . . . .
304 | 4 . . . # o . . . .
305 | 3 . . . # o . . . .
306 | 2 . . . # o . . . .
307 | 1 . . . # o . . . .
308 | A B C D E F G H J
309 | """, -9),
310 |
311 | ('spoilt', """\
312 | 9 . . . # o . . . .
313 | 8 . . . # o . . . .
314 | 7 . . . # o . . . .
315 | 6 . . . # o . . . .
316 | 5 . . . # o . . # .
317 | 4 . . . # o . . . .
318 | 3 . . . # o . . . .
319 | 2 . . . # o . . . .
320 | 1 . . . # o . . . .
321 | A B C D E F G H J
322 | """, 28),
323 |
324 | ('busy', """\
325 | 9 . . o . . o # . #
326 | 8 . o o o o o # . #
327 | 7 . o . o o # # # o
328 | 6 o . o # o # o o o
329 | 5 o o # # # # # o o
330 | 4 . o o # . o # o .
331 | 3 o # # # o o o o o
332 | 2 . o o # # # o o .
333 | 1 o . o # . # o o o
334 | A B C D E F G H J
335 | """, -26),
336 |
337 | ]
338 |
339 |
340 | setup_tests = [
341 |
342 | # code, black points, white points, empty points, diagram, is_legal
343 |
344 | ('blank',
345 | [],
346 | [],
347 | [],
348 | """\
349 | 9 . . . . . . . . .
350 | 8 . . . . . . . . .
351 | 7 . . . . . . . . .
352 | 6 . . . . . . . . .
353 | 5 . . . . . . . . .
354 | 4 . . . . . . . . .
355 | 3 . . . . . . . . .
356 | 2 . . . . . . . . .
357 | 1 . . . . . . . . .
358 | A B C D E F G H J
359 | """, True),
360 |
361 | ('simple',
362 | ['D4', 'D5'],
363 | ['B1', 'J9'],
364 | [],
365 | """\
366 | 9 . . . . . . . . o
367 | 8 . . . . . . . . .
368 | 7 . . . . . . . . .
369 | 6 . . . . . . . . .
370 | 5 . . . # . . . . .
371 | 4 . . . # . . . . .
372 | 3 . . . . . . . . .
373 | 2 . . . . . . . . .
374 | 1 . o . . . . . . .
375 | A B C D E F G H J
376 | """, True),
377 |
378 | ('illegal',
379 | ['A8', 'B9'],
380 | ['A9'],
381 | [],
382 | """\
383 | 9 . # . . . . . . .
384 | 8 # . . . . . . . .
385 | 7 . . . . . . . . .
386 | 6 . . . . . . . . .
387 | 5 . . . . . . . . .
388 | 4 . . . . . . . . .
389 | 3 . . . . . . . . .
390 | 2 . . . . . . . . .
391 | 1 . . . . . . . . .
392 | A B C D E F G H J
393 | """, False),
394 |
395 | ]
396 |
--------------------------------------------------------------------------------
/sgfmill_tests/board_tests.py:
--------------------------------------------------------------------------------
1 | """Tests for boards.py and ascii_boards.py
2 |
3 | We test these together because it's convenient for later boards tests to use
4 | ascii_boards facilities.
5 |
6 | """
7 |
8 | from sgfmill.common import format_vertex, move_from_vertex
9 | from sgfmill import ascii_boards
10 | from sgfmill import boards
11 |
12 | from . import sgfmill_test_support
13 | from . import board_test_data
14 |
15 | def make_tests(suite):
16 | suite.addTests(sgfmill_test_support.make_simple_tests(globals()))
17 | for t in board_test_data.play_tests:
18 | suite.addTest(Play_test_TestCase(*t))
19 | for t in board_test_data.score_tests:
20 | suite.addTest(Score_test_TestCase(*t))
21 | for t in board_test_data.setup_tests:
22 | suite.addTest(Setup_test_TestCase(*t))
23 |
24 | def test_attributes(tc):
25 | b = boards.Board(5)
26 | tc.assertEqual(b.side, 5)
27 | tc.assertEqual(
28 | b.board_points,
29 | [(0, 0), (0, 1), (0, 2), (0, 3), (0, 4),
30 | (1, 0), (1, 1), (1, 2), (1, 3), (1, 4),
31 | (2, 0), (2, 1), (2, 2), (2, 3), (2, 4),
32 | (3, 0), (3, 1), (3, 2), (3, 3), (3, 4),
33 | (4, 0), (4, 1), (4, 2), (4, 3), (4, 4)])
34 |
35 | def test_basics(tc):
36 | tc.assertRaises(ValueError, boards.Board, 1)
37 | tc.assertRaises(ValueError, boards.Board, 0)
38 | tc.assertRaises(ValueError, boards.Board, -1)
39 | tc.assertRaises((TypeError, ValueError), boards.Board, (19, 19))
40 |
41 | b = boards.Board(9)
42 |
43 | tc.assertTrue(b.is_empty())
44 | tc.assertCountEqual(b.list_occupied_points(), [])
45 |
46 | tc.assertEqual(b.get(2, 3), None)
47 | b.play(2, 3, 'b')
48 | tc.assertEqual(b.get(2, 3), 'b')
49 | tc.assertFalse(b.is_empty())
50 | b.play(3, 4, 'w')
51 |
52 | with tc.assertRaises(ValueError):
53 | b.play(3, 4, 'w')
54 |
55 | with tc.assertRaises(ValueError):
56 | b.play(5, 2, None)
57 |
58 | tc.assertCountEqual(b.list_occupied_points(),
59 | [('b', (2, 3)), ('w', (3, 4))])
60 |
61 | def test_range_checks(tc):
62 | b = boards.Board(9)
63 | tc.assertRaises(IndexError, b.get, -1, 2)
64 | tc.assertRaises(IndexError, b.get, 9, 2)
65 | tc.assertRaises(IndexError, b.get, 2, -1)
66 | tc.assertRaises(IndexError, b.get, 2, 9)
67 | tc.assertRaises(IndexError, b.play, -1, 2, 'b')
68 | tc.assertRaises(IndexError, b.play, 9, 2, 'b')
69 | tc.assertRaises(IndexError, b.play, 2, -1, 'b')
70 | tc.assertRaises(IndexError, b.play, 2, 9, 'b')
71 | tc.assertEqual(b, boards.Board(9))
72 |
73 |
74 | _9x9_expected = """\
75 | 9 . . . . . . . . .
76 | 8 . . . . . . . . .
77 | 7 . . . . . . . . .
78 | 6 . . . . . . . . .
79 | 5 . . . . . . . . .
80 | 4 . . . . o . . . .
81 | 3 . . . # . . . . .
82 | 2 . . . . . . . . .
83 | 1 . . . . . . . . .
84 | A B C D E F G H J\
85 | """
86 |
87 | _13x13_expected = """\
88 | 13 . . . . . . . . . . . . .
89 | 12 . . . . . . . . . . . . .
90 | 11 . . . . . . . . . . . . .
91 | 10 . . . . . . . . . . . . .
92 | 9 . . . . . . . . . . . . .
93 | 8 . . . . . . . . . . . . .
94 | 7 . . . . . . . . . . . . .
95 | 6 . . . . . . . . . . . . .
96 | 5 . . . . . . . . . . . . .
97 | 4 . . . . o . . . . . . . .
98 | 3 . . . # . . . . . . . . .
99 | 2 . . . . . . . . . . . . .
100 | 1 . . . . . . . . . . . . .
101 | A B C D E F G H J K L M N\
102 | """
103 |
104 | def test_render_board_9x9(tc):
105 | b = boards.Board(9)
106 | b.play(2, 3, 'b')
107 | b.play(3, 4, 'w')
108 | tc.assertDiagramEqual(ascii_boards.render_board(b), _9x9_expected)
109 |
110 | def test_render_board_13x13(tc):
111 | b = boards.Board(13)
112 | b.play(2, 3, 'b')
113 | b.play(3, 4, 'w')
114 | tc.assertDiagramEqual(ascii_boards.render_board(b), _13x13_expected)
115 |
116 | def test_interpret_diagram(tc):
117 | b1 = boards.Board(9)
118 | b1.play(2, 3, 'b')
119 | b1.play(3, 4, 'w')
120 | b2 = ascii_boards.interpret_diagram(_9x9_expected, 9)
121 | tc.assertEqual(b1, b2)
122 | b3 = boards.Board(9)
123 | b4 = ascii_boards.interpret_diagram(_9x9_expected, 9, b3)
124 | tc.assertIs(b3, b4)
125 | tc.assertEqual(b1, b3)
126 | tc.assertRaisesRegex(ValueError, "board not empty",
127 | ascii_boards.interpret_diagram, _9x9_expected, 9, b3)
128 | b5 = boards.Board(19)
129 | tc.assertRaisesRegex(ValueError, "wrong board size, must be 9$",
130 | ascii_boards.interpret_diagram, _9x9_expected, 9, b5)
131 |
132 | tc.assertRaises(ValueError, ascii_boards.interpret_diagram, "nonsense", 9)
133 | b6 = ascii_boards.interpret_diagram(_13x13_expected, 13)
134 | tc.assertDiagramEqual(ascii_boards.render_board(b6), _13x13_expected)
135 |
136 | padded = "\n\n" + _9x9_expected + "\n\n"
137 | tc.assertEqual(b1, ascii_boards.interpret_diagram(padded, 9))
138 |
139 | def test_get_neighbours(tc):
140 | neighbours = boards._get_neighbours(0, 0, 5)
141 | tc.assertEqual(2, len(neighbours))
142 | tc.assertTrue((0, 1) in neighbours)
143 | tc.assertTrue((1, 0) in neighbours)
144 | tc.assertFalse((0, 0) in neighbours)
145 | tc.assertFalse((-1, 0) in neighbours)
146 | tc.assertFalse((0, -1) in neighbours)
147 |
148 | def test_get_neighbours_and_self(tc):
149 | neighbours = boards._get_neighbours(0, 0, 5)
150 | neighbour_and_self = boards._get_neighbours_and_self(0, 0, 5)
151 | tc.assertEqual(len(neighbours) + 1, len(neighbour_and_self))
152 | for n in neighbours:
153 | tc.assertTrue(n in neighbour_and_self)
154 | tc.assertTrue((0, 0) in neighbour_and_self)
155 |
156 | def test_copy(tc):
157 | b1 = boards.Board(9)
158 | b1.play(2, 3, 'b')
159 | b1.play(3, 4, 'w')
160 | b2 = b1.copy()
161 | tc.assertEqual(b1, b2)
162 | b2.play(5, 5, 'b')
163 | b2.play(2, 1, 'b')
164 | tc.assertNotEqual(b1, b2)
165 | b1.play(5, 5, 'b')
166 | b1.play(2, 1, 'b')
167 | tc.assertEqual(b1, b2)
168 |
169 | def test_full_board_selfcapture(tc):
170 | b = boards.Board(9)
171 | tc.assertTrue(b.is_empty())
172 | tc.assertCountEqual(b.list_occupied_points(), [])
173 | for row in range(9):
174 | for col in range(9):
175 | b.play(row, col, 'b')
176 | tc.assertEqual(b, boards.Board(9))
177 | tc.assertIs(b.is_empty(), True)
178 |
179 | def test_apply_setup_range_checks(tc):
180 | b = boards.Board(9)
181 | tc.assertRaises(IndexError, b.apply_setup, [(1, 1), (9, 2)], [], [])
182 | tc.assertRaises(IndexError, b.apply_setup, [], [(2, 2), (2, -3)], [])
183 | tc.assertRaises(IndexError, b.apply_setup, [], [], [(3, 3), (-3, 2)])
184 | tc.assertEqual(b, boards.Board(9))
185 |
186 |
187 | class Play_test_TestCase(sgfmill_test_support.Sgfmill_ParameterisedTestCase):
188 | """Check final position reached by playing a sequence of moves."""
189 | test_name = "play_test"
190 | parameter_names = ('moves', 'diagram', 'ko_vertex', 'score')
191 |
192 | def runTest(self):
193 | b = boards.Board(9)
194 | ko_point = None
195 | for move in self.moves:
196 | colour, vertex = move.split()
197 | colour = colour.lower()
198 | row, col = move_from_vertex(vertex, b.side)
199 | ko_point = b.play(row, col, colour)
200 | self.assertBoardEqual(b, self.diagram)
201 | if ko_point is None:
202 | ko_vertex = None
203 | else:
204 | ko_vertex = format_vertex(ko_point)
205 | self.assertEqual(ko_vertex, self.ko_vertex, "wrong ko point")
206 | self.assertEqual(b.area_score(), self.score, "wrong score")
207 |
208 |
209 | class Score_test_TestCase(sgfmill_test_support.Sgfmill_ParameterisedTestCase):
210 | """Check score of a diagram."""
211 | test_name = "score_test"
212 | parameter_names = ('diagram', 'score')
213 |
214 | def runTest(self):
215 | b = ascii_boards.interpret_diagram(self.diagram, 9)
216 | self.assertEqual(b.area_score(), self.score, "wrong score")
217 |
218 |
219 | class Setup_test_TestCase(sgfmill_test_support.Sgfmill_ParameterisedTestCase):
220 | """Check apply_setup()."""
221 | test_name = "setup_test"
222 | parameter_names = ('black_points', 'white_points', 'empty_points',
223 | 'diagram', 'is_legal')
224 |
225 | def runTest(self):
226 | def _interpret(moves):
227 | return [move_from_vertex(v, b.side) for v in moves]
228 |
229 | b = boards.Board(9)
230 | is_legal = b.apply_setup(_interpret(self.black_points),
231 | _interpret(self.white_points),
232 | _interpret(self.empty_points))
233 | self.assertBoardEqual(b, self.diagram)
234 | if self.is_legal:
235 | self.assertTrue(is_legal, "setup should be considered legal")
236 | else:
237 | self.assertFalse(is_legal, "setup should be considered illegal")
238 |
--------------------------------------------------------------------------------
/sgfmill_tests/common_tests.py:
--------------------------------------------------------------------------------
1 | """Tests for common.py."""
2 |
3 | import string
4 |
5 | from . import sgfmill_test_support
6 |
7 | from sgfmill import common
8 |
9 | def make_tests(suite):
10 | suite.addTests(sgfmill_test_support.make_simple_tests(globals()))
11 |
12 |
13 | def test_opponent_of(tc):
14 | oo = common.opponent_of
15 | tc.assertEqual(oo('b'), 'w')
16 | tc.assertEqual(oo('w'), 'b')
17 | tc.assertRaises(ValueError, oo, 'x')
18 | tc.assertRaises(ValueError, oo, None)
19 | tc.assertRaises(ValueError, oo, 'B')
20 |
21 | def test_colour_name(tc):
22 | cn = common.colour_name
23 | tc.assertEqual(cn('b'), 'black')
24 | tc.assertEqual(cn('w'), 'white')
25 | tc.assertRaises(ValueError, cn, 'x')
26 | tc.assertRaises(ValueError, cn, None)
27 | tc.assertRaises(ValueError, cn, 'B')
28 |
29 | def test_column_letters(tc):
30 | tc.assertEqual(common.column_letters,
31 | "".join(c for c in string.ascii_uppercase if c != 'I'))
32 |
33 | def test_format_vertex(tc):
34 | fv = common.format_vertex
35 | tc.assertEqual(fv(None), "pass")
36 | tc.assertEqual(fv((0, 0)), "A1")
37 | tc.assertEqual(fv((8, 8)), "J9")
38 | tc.assertEqual(fv((1, 5)), "F2")
39 | tc.assertEqual(fv((24, 24)), "Z25")
40 | tc.assertRaises(ValueError, fv, (-1, 2))
41 | tc.assertRaises(ValueError, fv, (2, -1))
42 | tc.assertRaises(ValueError, fv, (25, 1))
43 | tc.assertRaises(ValueError, fv, (1, 25))
44 |
45 | def test_format_vertex_list(tc):
46 | fvl = common.format_vertex_list
47 | tc.assertEqual(fvl([]), "")
48 | tc.assertEqual(fvl([(0, 0)]), "A1")
49 | tc.assertEqual(fvl([(0, 0), (1, 5)]), "A1,F2")
50 | tc.assertEqual(fvl([(0, 0), None, (1, 5)]), "A1,pass,F2")
51 |
52 | def test_move_from_vertex(tc):
53 | cv = common.move_from_vertex
54 | tc.assertEqual(cv("pass", 9), None)
55 | tc.assertEqual(cv("pAss", 9), None)
56 | tc.assertEqual(cv("A1", 9), (0, 0))
57 | tc.assertEqual(cv("a1", 9), (0, 0))
58 | tc.assertEqual(cv("A01", 9), (0, 0))
59 | tc.assertEqual(cv("J9", 9), (8, 8))
60 | tc.assertEqual(cv("M11", 19), (10, 11))
61 | tc.assertRaises(ValueError, cv, "M11", 9)
62 | tc.assertRaises(ValueError, cv, "K9", 9)
63 | tc.assertRaises(ValueError, cv, "J10", 9)
64 | tc.assertRaises(ValueError, cv, "I5", 9)
65 | tc.assertRaises(ValueError, cv, "", 9)
66 | tc.assertRaises(ValueError, cv, "29", 9)
67 | tc.assertRaises(ValueError, cv, "@9", 9)
68 | tc.assertRaises(ValueError, cv, "A-3", 9)
69 | tc.assertRaises(ValueError, cv, None, 9)
70 | tc.assertRaises(ValueError, cv, "A1", 0)
71 | tc.assertRaises(ValueError, cv, "A1", 30)
72 |
73 |
--------------------------------------------------------------------------------
/sgfmill_tests/run_sgfmill_testsuite.py:
--------------------------------------------------------------------------------
1 | """Construct and run the sgfmill testsuite."""
2 |
3 | import unittest
4 | import sys
5 | from collections import defaultdict
6 | from optparse import OptionParser
7 |
8 | test_modules = [
9 | 'common_tests',
10 | 'board_tests',
11 | 'sgf_grammar_tests',
12 | 'sgf_properties_tests',
13 | 'sgf_tests',
14 | 'sgf_moves_tests',
15 | ]
16 |
17 | def get_test_module(name):
18 | """Import the specified sgfmill_tests module and return it."""
19 | dotted_name = "sgfmill_tests." + name
20 | __import__(dotted_name)
21 | return sys.modules[dotted_name]
22 |
23 | def run_testsuite(suite, failfast, buffer):
24 | """Run the specified testsuite.
25 |
26 | suite -- TestSuite
27 | failfast -- bool (stop at first failing test)
28 | buffer -- bool (show stderr/stdout only for failing tests)
29 |
30 | Output is to stderr
31 |
32 | """
33 | try:
34 | # This gives 'catchbreak' behaviour
35 | unittest.signals.installHandler()
36 | except Exception:
37 | pass
38 | runner = unittest.TextTestRunner(failfast=failfast, buffer=buffer)
39 | runner.run(suite)
40 |
41 | class UnknownTest(Exception):
42 | """Unknown test module or test name."""
43 |
44 | def make_testsuite(module_names, tests_by_module):
45 | """Import testsuite modules and make the TestCases.
46 |
47 | module_names -- set of module names (empty means all)
48 | tests_by_module -- map module_name -> set of test names (empty means all)
49 |
50 | Returns a TestSuite.
51 |
52 | Test names are as given by test.id()
53 | For function-based tests, that means .
54 | For parameterised tests, it's .:
55 |
56 | The tests in the returned suite are always in their 'natural' order; the
57 | order of command line items has no effect.
58 |
59 | Raises UnknownTest if a specified test or test module doesn't exist.
60 |
61 | """
62 | result = unittest.TestSuite()
63 | for module_name in sorted(module_names):
64 | if module_name not in test_modules:
65 | raise UnknownTest("unknown module: %s" % module_name)
66 | for module_name in test_modules:
67 | if module_names and (module_name not in module_names):
68 | continue
69 | mdl = get_test_module(module_name)
70 | suite = unittest.TestSuite()
71 | mdl.make_tests(suite)
72 | test_names = tests_by_module[module_name]
73 | if not test_names:
74 | result.addTests(suite)
75 | continue
76 | existing = set(test.id() for test in suite)
77 | for test_name in sorted(test_names):
78 | if test_name not in existing:
79 | raise UnknownTest("unknown test: %s" % test_name)
80 | for test in suite:
81 | if test.id() in test_names:
82 | result.addTest(test)
83 | return result
84 |
85 | def interpret_args(args):
86 | """Interpret command-line arguments.
87 |
88 | Returns a pair (module_names, tests_by_module), for make_testsuite().
89 |
90 | """
91 | tests_by_module = defaultdict(set)
92 | module_names = set()
93 | for arg in args:
94 | module_name, is_compound, _ = arg.partition(".")
95 | module_names.add(module_name)
96 | if is_compound:
97 | tests_by_module[module_name].add(arg)
98 | return module_names, tests_by_module
99 |
100 | def run(argv):
101 | parser = OptionParser(usage="%prog [options] [module|module.test] ...")
102 | parser.add_option("-f", "--failfast", action="store_true",
103 | help="stop after first test")
104 | parser.add_option("-p", "--nobuffer", action="store_true",
105 | help="show stderr/stdout for successful tests")
106 | (options, args) = parser.parse_args(argv)
107 | module_names, tests_by_module = interpret_args(args)
108 | try:
109 | suite = make_testsuite(module_names, tests_by_module)
110 | except UnknownTest as e:
111 | parser.error(str(e))
112 | run_testsuite(suite, options.failfast, not options.nobuffer)
113 |
114 | if __name__ == "__main__":
115 | run(sys.argv[1:])
116 |
117 |
--------------------------------------------------------------------------------
/sgfmill_tests/sgf_grammar_tests.py:
--------------------------------------------------------------------------------
1 | """Tests for sgf_grammar.py."""
2 |
3 | from . import sgfmill_test_support
4 |
5 | from sgfmill import sgf_grammar
6 |
7 | def make_tests(suite):
8 | suite.addTests(sgfmill_test_support.make_simple_tests(globals()))
9 |
10 | def test_is_valid_property_identifier(tc):
11 | ivpi = sgf_grammar.is_valid_property_identifier
12 | tc.assertIs(ivpi("B"), True)
13 | tc.assertIs(ivpi("PB"), True)
14 | tc.assertIs(ivpi("ABCDEFGH"), True)
15 | tc.assertIs(ivpi("MULTIGOGM"), True)
16 | tc.assertIs(ivpi(64*"X"), True)
17 | tc.assertIs(ivpi(65*"X"), False)
18 | tc.assertIs(ivpi(""), False)
19 | tc.assertIs(ivpi("b"), False)
20 | tc.assertIs(ivpi("Player"), False)
21 | tc.assertIs(ivpi("P2"), False)
22 | tc.assertIs(ivpi(" PB"), False)
23 | tc.assertIs(ivpi("PB "), False)
24 | tc.assertIs(ivpi("P B"), False)
25 | tc.assertIs(ivpi("PB\x00"), False)
26 |
27 | def test_is_valid_property_value(tc):
28 | ivpv = sgf_grammar.is_valid_property_value
29 | tc.assertIs(ivpv(b""), True)
30 | tc.assertIs(ivpv(b"hello world"), True)
31 | tc.assertIs(ivpv(b"hello\nworld"), True)
32 | tc.assertIs(ivpv(b"hello \x00 world"), True)
33 | tc.assertIs(ivpv(b"hello \xa3 world"), True)
34 | tc.assertIs(ivpv(b"hello \xc2\xa3 world"), True)
35 | tc.assertIs(ivpv(b"hello \\-) world"), True)
36 | tc.assertIs(ivpv(b"hello (;[) world"), True)
37 | tc.assertIs(ivpv(b"[hello world]"), False)
38 | tc.assertIs(ivpv(b"hello ] world"), False)
39 | tc.assertIs(ivpv(b"hello \\] world"), True)
40 | tc.assertIs(ivpv(b"hello world \\"), False)
41 | tc.assertIs(ivpv(b"hello world \\\\"), True)
42 | tc.assertIs(ivpv(b"x" * 70000), True)
43 |
44 | def test_tokeniser(tc):
45 | tokenise = sgf_grammar.tokenise
46 |
47 | tc.assertEqual(tokenise(b"(;B[ah][]C[a\xa3b])")[0],
48 | [('D', b'('),
49 | ('D', b';'),
50 | ('I', b'B'),
51 | ('V', b'ah'),
52 | ('V', b''),
53 | ('I', b'C'),
54 | ('V', b'a\xa3b'),
55 | ('D', b')')])
56 |
57 | def check_complete(bb, *args):
58 | tokens, tail_index = tokenise(bb, *args)
59 | tc.assertEqual(tail_index, len(bb))
60 | return len(tokens)
61 |
62 | def check_incomplete(bb, *args):
63 | tokens, tail_index = tokenise(bb, *args)
64 | return len(tokens), tail_index
65 |
66 | # check surrounding junk
67 | tc.assertEqual(check_complete(b""), 0)
68 | tc.assertEqual(check_complete(b"junk (;B[ah])"), 5)
69 | tc.assertEqual(check_incomplete(b"junk"), (0, 0))
70 | tc.assertEqual(check_incomplete(b"junk (B[ah])"), (0, 0))
71 | tc.assertEqual(check_incomplete(b"(;B[ah]) junk"), (5, 8))
72 |
73 | # check paren-balance count
74 | tc.assertEqual(check_incomplete(b"(; ))(([ag]B C[ah])"), (3, 4))
75 | tc.assertEqual(check_incomplete(b"(;( )) (;)"), (5, 6))
76 | tc.assertEqual(check_incomplete(b"(;(()())) (;)"), (9, 9))
77 |
78 | # check start_position
79 | tc.assertEqual(check_complete(b"(; ))(;B[ah])", 4), 5)
80 | tc.assertEqual(check_complete(b"(; ))junk (;B[ah])", 4), 5)
81 |
82 | tc.assertEqual(check_complete(b"(;XX[abc][def]KO[];B[bc])"), 11)
83 | tc.assertEqual(check_complete(b"( ;XX[abc][def]KO[];B[bc])"), 11)
84 | tc.assertEqual(check_complete(b"(; XX[abc][def]KO[];B[bc])"), 11)
85 | tc.assertEqual(check_complete(b"(;XX [abc][def]KO[];B[bc])"), 11)
86 | tc.assertEqual(check_complete(b"(;XX[abc] [def]KO[];B[bc])"), 11)
87 | tc.assertEqual(check_complete(b"(;XX[abc][def] KO[];B[bc])"), 11)
88 | tc.assertEqual(check_complete(b"(;XX[abc][def]KO [];B[bc])"), 11)
89 | tc.assertEqual(check_complete(b"(;XX[abc][def]KO[] ;B[bc])"), 11)
90 | tc.assertEqual(check_complete(b"(;XX[abc][def]KO[]; B[bc])"), 11)
91 | tc.assertEqual(check_complete(b"(;XX[abc][def]KO[];B [bc])"), 11)
92 | tc.assertEqual(check_complete(b"(;XX[abc][def]KO[];B[bc] )"), 11)
93 |
94 | tc.assertEqual(check_complete(b"( ;\nB\t[ah]\f[ef]\v)"), 6)
95 | tc.assertEqual(check_complete(b"(;[Ran\xc2\xa3dom :\nstu@ff][ef]"), 4)
96 | tc.assertEqual(check_complete(b"(;[ah)])"), 4)
97 |
98 | # check PropIdent rule
99 | tc.assertEqual(check_complete(b"(;" + (8*b"X")), 3)
100 | tc.assertEqual(check_complete(b"(;" + (9*b"X")), 3)
101 | tc.assertEqual(check_complete(b"(;" + (64*b"X")), 3)
102 | tc.assertEqual(check_complete(b"(;" + (65*b"X")), 4)
103 |
104 | tc.assertEqual(check_incomplete(b"(;B[ag"), (3, 3))
105 | tc.assertEqual(check_incomplete(b"(;B[ag)"), (3, 3))
106 | tc.assertEqual(check_incomplete(b"(;+B[ag])"), (2, 2))
107 | tc.assertEqual(check_incomplete(b"(;B+[ag])"), (3, 3))
108 | tc.assertEqual(check_incomplete(b"(;B[ag]+)"), (4, 7))
109 |
110 | tc.assertEqual(check_complete(br"(;[ab \] cd][ef]"), 4)
111 | tc.assertEqual(check_complete(br"(;[ab \] cd\\][ef]"), 4)
112 | tc.assertEqual(check_complete(br"(;[ab \] cd\\\\][ef]"), 4)
113 | tc.assertEqual(check_complete(br"(;[ab \] \\\] cd][ef]"), 4)
114 | tc.assertEqual(check_incomplete(br"(;B[ag\])"), (3, 3))
115 | tc.assertEqual(check_incomplete(br"(;B[ag\\\])"), (3, 3))
116 |
117 | def test_tokeniser_lower_case_propidents(tc):
118 | tokenise = sgf_grammar.tokenise
119 |
120 | tc.assertEqual(tokenise(b"(;AddBlack[ag])")[0],
121 | [('D', b'('),
122 | ('D', b';'),
123 | ('I', b'AB'),
124 | ('V', b'ag'),
125 | ('D', b')')])
126 |
127 | def test_parser_structure(tc):
128 | parse_sgf_game = sgf_grammar.parse_sgf_game
129 |
130 | def shape(bb):
131 | coarse_game = parse_sgf_game(bb)
132 | return len(coarse_game.sequence), len(coarse_game.children)
133 |
134 | tc.assertEqual(shape(b"(;C[abc]KO[];B[bc])"), (2, 0))
135 | tc.assertEqual(shape(b"initial junk (;C[abc]KO[];B[bc])"), (2, 0))
136 | tc.assertEqual(shape(b"(;C[abc]KO[];B[bc]) final junk"), (2, 0))
137 | tc.assertEqual(shape(b"(;C[abc]KO[];B[bc]) (;B[ag])"), (2, 0))
138 |
139 | tc.assertRaisesRegex(ValueError, "no SGF data found",
140 | parse_sgf_game, b"")
141 | tc.assertRaisesRegex(ValueError, "no SGF data found",
142 | parse_sgf_game, b"junk")
143 | tc.assertRaisesRegex(ValueError, "no SGF data found",
144 | parse_sgf_game, b"()")
145 | tc.assertRaisesRegex(ValueError, "no SGF data found",
146 | parse_sgf_game, b"(B[ag])")
147 | tc.assertRaisesRegex(ValueError, "no SGF data found",
148 | parse_sgf_game, b"B[ag]")
149 | tc.assertRaisesRegex(ValueError, "no SGF data found",
150 | parse_sgf_game, b"[ag]")
151 |
152 | tc.assertEqual(shape(b"(;C[abc]AB[ab][bc];B[bc])"), (2, 0))
153 | tc.assertEqual(shape(b"(;C[abc] AB[ab]\n[bc]\t;B[bc])"), (2, 0))
154 | tc.assertEqual(shape(b"(;C[abc]KO[];;B[bc])"), (3, 0))
155 | tc.assertEqual(shape(b"(;)"), (1, 0))
156 |
157 | tc.assertRaisesRegex(ValueError, "property with no values",
158 | parse_sgf_game, b"(;B)")
159 | tc.assertRaisesRegex(ValueError, "unexpected value",
160 | parse_sgf_game, b"(;[ag])")
161 | tc.assertRaisesRegex(ValueError, "unexpected value",
162 | parse_sgf_game, b"(;[ag][ah])")
163 | tc.assertRaisesRegex(ValueError, "unexpected value",
164 | parse_sgf_game, b"(;[B][ag])")
165 | tc.assertRaisesRegex(ValueError, "unexpected end of SGF data",
166 | parse_sgf_game, b"(;B[ag]")
167 | tc.assertRaisesRegex(ValueError, "unexpected end of SGF data",
168 | parse_sgf_game, b"(;B[ag][)]")
169 | tc.assertRaisesRegex(ValueError, "property with no values",
170 | parse_sgf_game, b"(;B;W[ah])")
171 | tc.assertRaisesRegex(ValueError, "unexpected value",
172 | parse_sgf_game, b"(;B[ag](;[ah]))")
173 | tc.assertRaisesRegex(ValueError, "property with no values",
174 | parse_sgf_game, b"(;B W[ag])")
175 |
176 | def test_parser_tree_structure(tc):
177 | parse_sgf_game = sgf_grammar.parse_sgf_game
178 |
179 | def shape(bb):
180 | coarse_game = parse_sgf_game(bb)
181 | return len(coarse_game.sequence), len(coarse_game.children)
182 |
183 | tc.assertEqual(shape(b"(;C[abc]AB[ab](;B[bc]))"), (1, 1))
184 | tc.assertEqual(shape(b"(;C[abc]AB[ab](;B[bc])))"), (1, 1))
185 | tc.assertEqual(shape(b"(;C[abc]AB[ab](;B[bc])(;B[bd]))"), (1, 2))
186 |
187 | def shapetree(bb):
188 | def _shapetree(coarse_game):
189 | return (
190 | len(coarse_game.sequence),
191 | [_shapetree(pg) for pg in coarse_game.children])
192 | return _shapetree(parse_sgf_game(bb))
193 |
194 | tc.assertEqual(shapetree(b"(;C[abc]AB[ab](;B[bc])))"),
195 | (1, [(1, [])])
196 | )
197 | tc.assertEqual(shapetree(b"(;C[abc]AB[ab](;B[bc]))))"),
198 | (1, [(1, [])])
199 | )
200 | tc.assertEqual(shapetree(b"(;C[abc]AB[ab](;B[bc])(;B[bd])))"),
201 | (1, [(1, []), (1, [])])
202 | )
203 | tc.assertEqual(shapetree(b"""
204 | (;C[abc]AB[ab];C[];C[]
205 | (;B[bc])
206 | (;B[bd];W[ca] (;B[da])(;B[db];W[ea]) )
207 | )"""),
208 | (3, [
209 | (1, []),
210 | (2, [(1, []), (2, [])])
211 | ])
212 | )
213 |
214 | tc.assertRaisesRegex(ValueError, "unexpected end of SGF data",
215 | parse_sgf_game, b"(;B[ag];W[ah](;B[ai])")
216 | tc.assertRaisesRegex(ValueError, "empty sequence",
217 | parse_sgf_game, b"(;B[ag];())")
218 | tc.assertRaisesRegex(ValueError, "empty sequence",
219 | parse_sgf_game, b"(;B[ag]())")
220 | tc.assertRaisesRegex(ValueError, "empty sequence",
221 | parse_sgf_game, b"(;B[ag]((;W[ah])(;W[ai]))")
222 | tc.assertRaisesRegex(ValueError, "unexpected node",
223 | parse_sgf_game, b"(;B[ag];W[ah](;B[ai]);W[bd])")
224 | tc.assertRaisesRegex(ValueError, "property value outside a node",
225 | parse_sgf_game, b"(;B[ag];(W[ah];B[ai]))")
226 | tc.assertRaisesRegex(ValueError, "property value outside a node",
227 | parse_sgf_game, b"(;B[ag](;W[ah];)B[ai])")
228 | tc.assertRaisesRegex(ValueError, "property value outside a node",
229 | parse_sgf_game, b"(;B[ag](;W[ah])(B[ai]))")
230 |
231 | def test_parser_properties(tc):
232 | parse_sgf_game = sgf_grammar.parse_sgf_game
233 |
234 | def props(bb):
235 | coarse_game = parse_sgf_game(bb)
236 | return coarse_game.sequence
237 |
238 | tc.assertEqual(props(b"(;C[abc]KO[]AB[ai][bh][ee];B[ bc])"),
239 | [{'C': [b'abc'], 'KO': [b''], 'AB': [b'ai', b'bh', b'ee']},
240 | {'B': [b' bc']}])
241 |
242 | tc.assertEqual(props(br"(;C[ab \] \) cd\\])"),
243 | [{'C': [br"ab \] \) cd\\"]}])
244 |
245 | tc.assertEqual(props(b"(;XX[1]YY[2]XX[3]YY[4])"),
246 | [{'XX': [b'1', b'3'], 'YY' : [b'2', b'4']}])
247 |
248 | def test_parse_sgf_collection(tc):
249 | parse_sgf_collection = sgf_grammar.parse_sgf_collection
250 |
251 | tc.assertRaisesRegex(ValueError, "no SGF data found",
252 | parse_sgf_collection, b"")
253 | tc.assertRaisesRegex(ValueError, "no SGF data found",
254 | parse_sgf_collection, b"()")
255 |
256 | games = parse_sgf_collection(b"(;C[abc]AB[ab];X[];X[](;B[bc]))")
257 | tc.assertEqual(len(games), 1)
258 | tc.assertEqual(len(games[0].sequence), 3)
259 |
260 | games = parse_sgf_collection(b"(;X[1];X[2];X[3](;B[bc])) (;Y[1];Y[2])")
261 | tc.assertEqual(len(games), 2)
262 | tc.assertEqual(len(games[0].sequence), 3)
263 | tc.assertEqual(len(games[1].sequence), 2)
264 |
265 | games = parse_sgf_collection(
266 | b"dummy (;X[1];X[2];X[3](;B[bc])) junk (;Y[1];Y[2]) Nonsense")
267 | tc.assertEqual(len(games), 2)
268 | tc.assertEqual(len(games[0].sequence), 3)
269 | tc.assertEqual(len(games[1].sequence), 2)
270 |
271 | games = parse_sgf_collection(
272 | b"(( (;X[1];X[2];X[3](;B[bc])) ();) (;Y[1];Y[2]) )(Nonsense")
273 | tc.assertEqual(len(games), 2)
274 | tc.assertEqual(len(games[0].sequence), 3)
275 | tc.assertEqual(len(games[1].sequence), 2)
276 |
277 | with tc.assertRaises(ValueError) as ar:
278 | parse_sgf_collection(
279 | b"(( (;X[1];X[2];X[3](;B[bc])) ();) (;Y[1];Y[2]")
280 | tc.assertEqual(str(ar.exception),
281 | "error parsing game 1: unexpected end of SGF data")
282 |
283 |
284 | def test_parse_compose(tc):
285 | pc = sgf_grammar.parse_compose
286 | tc.assertEqual(pc(b"word"), (b"word", None))
287 | tc.assertEqual(pc(b"word:"), (b"word", b""))
288 | tc.assertEqual(pc(b"word:?"), (b"word", b"?"))
289 | tc.assertEqual(pc(b"word:123"), (b"word", b"123"))
290 | tc.assertEqual(pc(b"word:123:456"), (b"word", b"123:456"))
291 | tc.assertEqual(pc(b":123"), (b"", b"123"))
292 | tc.assertEqual(pc(br"word\:more"), (br"word\:more", None))
293 | tc.assertEqual(pc(br"word\:more:?"), (br"word\:more", b"?"))
294 | tc.assertEqual(pc(br"word\\:more:?"), (b"word\\\\", b"more:?"))
295 | tc.assertEqual(pc(br"word\\\:more:?"), (br"word\\\:more", b"?"))
296 | tc.assertEqual(pc(b"word\\\nmore:123"), (b"word\\\nmore", b"123"))
297 |
298 | def test_text_value(tc):
299 | text_value = sgf_grammar.text_value
300 | tc.assertEqual(text_value(b"abc "), b"abc ")
301 | tc.assertEqual(text_value(b"ab c"), b"ab c")
302 | tc.assertEqual(text_value(b"ab\tc"), b"ab c")
303 | tc.assertEqual(text_value(b"ab \tc"), b"ab c")
304 | tc.assertEqual(text_value(b"ab\nc"), b"ab\nc")
305 | tc.assertEqual(text_value(b"ab\\\nc"), b"abc")
306 | tc.assertEqual(text_value(b"ab\\\\\nc"), b"ab\\\nc")
307 | tc.assertEqual(text_value(b"ab\xa0c"), b"ab\xa0c")
308 |
309 | tc.assertEqual(text_value(b"ab\rc"), b"ab\nc")
310 | tc.assertEqual(text_value(b"ab\r\nc"), b"ab\nc")
311 | tc.assertEqual(text_value(b"ab\n\rc"), b"ab\nc")
312 | tc.assertEqual(text_value(b"ab\r\n\r\nc"), b"ab\n\nc")
313 | tc.assertEqual(text_value(b"ab\r\n\r\n\rc"), b"ab\n\n\nc")
314 | tc.assertEqual(text_value(b"ab\\\r\nc"), b"abc")
315 | tc.assertEqual(text_value(b"ab\\\n\nc"), b"ab\nc")
316 |
317 | tc.assertEqual(text_value(b"ab\\\tc"), b"ab c")
318 |
319 | # These can't actually appear as SGF PropValues; anything sane will do
320 | tc.assertEqual(text_value(b"abc\\"), b"abc")
321 | tc.assertEqual(text_value(b"abc]"), b"abc]")
322 |
323 | def test_simpletext_value(tc):
324 | simpletext_value = sgf_grammar.simpletext_value
325 | tc.assertEqual(simpletext_value(b"abc "), b"abc ")
326 | tc.assertEqual(simpletext_value(b"ab c"), b"ab c")
327 | tc.assertEqual(simpletext_value(b"ab\tc"), b"ab c")
328 | tc.assertEqual(simpletext_value(b"ab \tc"), b"ab c")
329 | tc.assertEqual(simpletext_value(b"ab\nc"), b"ab c")
330 | tc.assertEqual(simpletext_value(b"ab\\\nc"), b"abc")
331 | tc.assertEqual(simpletext_value(b"ab\\\\\nc"), b"ab\\ c")
332 | tc.assertEqual(simpletext_value(b"ab\xa0c"), b"ab\xa0c")
333 |
334 | tc.assertEqual(simpletext_value(b"ab\rc"), b"ab c")
335 | tc.assertEqual(simpletext_value(b"ab\r\nc"), b"ab c")
336 | tc.assertEqual(simpletext_value(b"ab\n\rc"), b"ab c")
337 | tc.assertEqual(simpletext_value(b"ab\r\n\r\nc"), b"ab c")
338 | tc.assertEqual(simpletext_value(b"ab\r\n\r\n\rc"), b"ab c")
339 | tc.assertEqual(simpletext_value(b"ab\\\r\nc"), b"abc")
340 | tc.assertEqual(simpletext_value(b"ab\\\n\nc"), b"ab c")
341 |
342 | tc.assertEqual(simpletext_value(b"ab\\\tc"), b"ab c")
343 |
344 | # These can't actually appear as SGF PropValues; anything sane will do
345 | tc.assertEqual(simpletext_value(b"abc\\"), b"abc")
346 | tc.assertEqual(simpletext_value(b"abc]"), b"abc]")
347 |
348 | def test_escape_text(tc):
349 | tc.assertEqual(sgf_grammar.escape_text(b"abc"), b"abc")
350 | tc.assertEqual(sgf_grammar.escape_text(br"a\bc"), br"a\\bc")
351 | tc.assertEqual(sgf_grammar.escape_text(br"ab[c]"), br"ab[c\]")
352 | tc.assertEqual(sgf_grammar.escape_text(br"a\]bc"), br"a\\\]bc")
353 |
354 | def test_text_roundtrip(tc):
355 | def roundtrip(bb):
356 | return sgf_grammar.text_value(sgf_grammar.escape_text(bb))
357 | tc.assertEqual(roundtrip(br"abc"), br"abc")
358 | tc.assertEqual(roundtrip(br"a\bc"), br"a\bc")
359 | tc.assertEqual(roundtrip(b"abc\\"), b"abc\\")
360 | tc.assertEqual(roundtrip(b"ab]c"), b"ab]c")
361 | tc.assertEqual(roundtrip(b"abc]"), b"abc]")
362 | tc.assertEqual(roundtrip(br"abc\]"), br"abc\]")
363 | tc.assertEqual(roundtrip(b"ab\nc"), b"ab\nc")
364 | tc.assertEqual(roundtrip(b"ab\n c"), b"ab\n c")
365 |
366 | tc.assertEqual(roundtrip(b"ab\tc"), b"ab c")
367 | tc.assertEqual(roundtrip(b"ab\r\nc\n"), b"ab\nc\n")
368 |
369 | def test_serialise_game_tree(tc):
370 | serialised = (b"(;AB[aa][ab][ac]C[comment \xa3];W[ab];C[];C[]"
371 | b"(;B[bc])(;B[bd];W[ca](;B[da])(;B[db];\n"
372 | b"W[ea])))\n")
373 | coarse_game = sgf_grammar.parse_sgf_game(serialised)
374 | tc.assertEqual(sgf_grammar.serialise_game_tree(coarse_game), serialised)
375 | tc.assertEqual(sgf_grammar.serialise_game_tree(coarse_game, wrap=None),
376 | serialised.replace(b"\n", b"")+b"\n")
377 |
378 | def test_parse_bytearray(tc):
379 | # We document that these functions accept a "bytes-like object"
380 | bb = b'(;C[abc]AB[ab](;B[bc])(;B[bd])))'
381 | cg1 = sgf_grammar.parse_sgf_game(bb)
382 | cg2 = sgf_grammar.parse_sgf_game(bytearray(bb))
383 | tc.assertEqual(sgf_grammar.serialise_game_tree(cg1),
384 | sgf_grammar.serialise_game_tree(cg2))
385 |
386 |
--------------------------------------------------------------------------------
/sgfmill_tests/sgf_moves_tests.py:
--------------------------------------------------------------------------------
1 | from . import sgfmill_test_support
2 |
3 | from sgfmill import ascii_boards
4 | from sgfmill import boards
5 | from sgfmill import sgf
6 | from sgfmill import sgf_moves
7 |
8 | def make_tests(suite):
9 | suite.addTests(sgfmill_test_support.make_simple_tests(globals()))
10 |
11 |
12 | SAMPLE_SGF = """\
13 | (;AP[testsuite:0]CA[utf-8]DT[2009-06-06]FF[4]GM[1]KM[7.5]PB[Black engine]
14 | PL[B]PW[White engine]RE[W+R]SZ[9]AB[ai][bh][ee]AW[fc][gc];B[dg];W[ef]C[comment
15 | on two lines];B[];W[tt]C[Final comment])
16 | """
17 |
18 | DIAGRAM1 = """\
19 | 9 . . . . . . . . .
20 | 8 . . . . . . . . .
21 | 7 . . . . . o o . .
22 | 6 . . . . . . . . .
23 | 5 . . . . # . . . .
24 | 4 . . . . . . . . .
25 | 3 . . . . . . . . .
26 | 2 . # . . . . . . .
27 | 1 # . . . . . . . .
28 | A B C D E F G H J
29 | """
30 |
31 | DIAGRAM2 = """\
32 | 9 . . . . . . . . .
33 | 8 . . . . . . . . .
34 | 7 . . . . . . . . .
35 | 6 . . . . . . . . .
36 | 5 . . . . . . . . .
37 | 4 . . . . # . . . .
38 | 3 . . . . . . . . .
39 | 2 . . # . . . . . .
40 | 1 . . . . . . . . .
41 | A B C D E F G H J
42 | """
43 |
44 |
45 | def test_get_setup_and_moves(tc):
46 | g1 = sgf.Sgf_game.from_string(SAMPLE_SGF)
47 | board1, plays1 = sgf_moves.get_setup_and_moves(g1)
48 | tc.assertBoardEqual(board1, DIAGRAM1)
49 | tc.assertEqual(plays1,
50 | [('b', (2, 3)), ('w', (3, 4)), ('b', None), ('w', None)])
51 |
52 | g2 = sgf.Sgf_game(size=9)
53 | root = g2.get_root()
54 | root.set("AB", [(1, 2), (3, 4)])
55 | node = g2.extend_main_sequence()
56 | node.set("B", (5, 6))
57 | node = g2.extend_main_sequence()
58 | node.set("W", (5, 7))
59 | board2, plays2 = sgf_moves.get_setup_and_moves(g2)
60 | tc.assertBoardEqual(board2, DIAGRAM2)
61 | tc.assertEqual(plays2,
62 | [('b', (5, 6)), ('w', (5, 7))])
63 |
64 | g3 = sgf.Sgf_game.from_string("(;AB[ab][ba]AW[aa])")
65 | tc.assertRaisesRegex(ValueError, "setup position not legal",
66 | sgf_moves.get_setup_and_moves, g3)
67 |
68 | g4 = sgf.Sgf_game.from_string("(;SZ[9];B[ab];AW[bc])")
69 | tc.assertRaisesRegex(ValueError, "setup properties after the root node",
70 | sgf_moves.get_setup_and_moves, g4)
71 |
72 | g5 = sgf.Sgf_game.from_string("(;SZ[26];B[ab];W[bc])")
73 | board5, plays5 = sgf_moves.get_setup_and_moves(g5)
74 | tc.assertEqual(plays5,
75 | [('b', (24, 0)), ('w', (23, 1))])
76 |
77 |
78 | def test_get_setup_and_moves_move_in_root(tc):
79 | # A move in the root node is allowed (though deprecated) if there are no
80 | # setup stones.
81 | g1 = sgf.Sgf_game(size=9)
82 | root = g1.get_root()
83 | root.set("B", (1, 2))
84 | node = g1.extend_main_sequence()
85 | node.set("W", (3, 4))
86 | board1, plays1 = sgf_moves.get_setup_and_moves(g1)
87 | tc.assertTrue(board1.is_empty())
88 | tc.assertEqual(plays1,
89 | [('b', (1, 2)), ('w', (3, 4))])
90 |
91 | g2 = sgf.Sgf_game(size=9)
92 | root = g2.get_root()
93 | root.set("B", (1, 2))
94 | root.set("AW", [(3, 3)])
95 | node = g2.extend_main_sequence()
96 | node.set("W", (3, 4))
97 | tc.assertRaisesRegex(ValueError, "mixed setup and moves in root node",
98 | sgf_moves.get_setup_and_moves, g2)
99 |
100 | def test_get_setup_and_moves_board_provided(tc):
101 | b = boards.Board(9)
102 | g1 = sgf.Sgf_game.from_string(SAMPLE_SGF)
103 | board1, plays1 = sgf_moves.get_setup_and_moves(g1, b)
104 | tc.assertIs(board1, b)
105 | tc.assertBoardEqual(board1, DIAGRAM1)
106 | tc.assertEqual(plays1,
107 | [('b', (2, 3)), ('w', (3, 4)), ('b', None), ('w', None)])
108 | tc.assertRaisesRegex(ValueError, "board not empty",
109 | sgf_moves.get_setup_and_moves, g1, b)
110 | b2 = boards.Board(19)
111 | tc.assertRaisesRegex(ValueError, "wrong board size, must be 9$",
112 | sgf_moves.get_setup_and_moves, g1, b2)
113 |
114 |
115 | def test_set_initial_position(tc):
116 | board = ascii_boards.interpret_diagram(DIAGRAM1, 9)
117 | sgf_game = sgf.Sgf_game(9)
118 | sgf_moves.set_initial_position(sgf_game, board)
119 | root = sgf_game.get_root()
120 | tc.assertEqual(root.get("AB"), {(0, 0), (1, 1), (4, 4)})
121 | tc.assertEqual(root.get("AW"), {(6, 5), (6, 6)})
122 | tc.assertRaises(KeyError, root.get, 'AE')
123 |
124 | def test_indicate_first_player(tc):
125 | # Normal game
126 | g1 = sgf.Sgf_game.from_bytes(b"(;FF[4]GM[1]SZ[9];B[aa];W[ab])")
127 | sgf_moves.indicate_first_player(g1)
128 | tc.assertEqual(g1.serialise(),
129 | b"(;FF[4]GM[1]SZ[9];B[aa];W[ab])\n")
130 | # White plays first
131 | g2 = sgf.Sgf_game.from_bytes(b"(;FF[4]GM[1]SZ[9];W[aa];B[ab])")
132 | sgf_moves.indicate_first_player(g2)
133 | tc.assertEqual(g2.serialise(),
134 | b"(;FF[4]GM[1]PL[W]SZ[9];W[aa];B[ab])\n")
135 | # No moves
136 | g3 = sgf.Sgf_game.from_bytes(b"(;FF[4]GM[1]SZ[9];C[no game])")
137 | sgf_moves.indicate_first_player(g3)
138 | tc.assertEqual(g3.serialise(),
139 | b"(;FF[4]GM[1]SZ[9];C[no game])\n")
140 | # Normal handicap game
141 | g4 = sgf.Sgf_game.from_bytes(b"(;FF[4]GM[1]HA[5]SZ[9];W[aa];B[ab])")
142 | sgf_moves.indicate_first_player(g4)
143 | tc.assertEqual(g4.serialise(),
144 | b"(;FF[4]GM[1]HA[5]SZ[9];W[aa];B[ab])\n")
145 | # Handicap game, black plays first
146 | g5 = sgf.Sgf_game.from_bytes(b"(;FF[4]GM[1]HA[5]SZ[9];B[aa];W[ab])")
147 | sgf_moves.indicate_first_player(g5)
148 | tc.assertEqual(g5.serialise(),
149 | b"(;FF[4]GM[1]HA[5]PL[B]SZ[9];B[aa];W[ab])\n")
150 | # White setup stones
151 | g6 = sgf.Sgf_game.from_bytes(b"(;AW[bc]FF[4]GM[1]SZ[9];B[aa];W[ab])")
152 | sgf_moves.indicate_first_player(g6)
153 | tc.assertEqual(g6.serialise(),
154 | b"(;FF[4]AW[bc]GM[1]PL[B]SZ[9];B[aa];W[ab])\n")
155 | # Black setup stones
156 | g7 = sgf.Sgf_game.from_bytes(b"(;AB[bc]FF[4]GM[1]SZ[9];B[aa];W[ab])")
157 | sgf_moves.indicate_first_player(g7)
158 | tc.assertEqual(g7.serialise(),
159 | b"(;FF[4]AB[bc]GM[1]PL[B]SZ[9];B[aa];W[ab])\n")
160 | # Black setup stones, handicap, white plays first
161 | g8 = sgf.Sgf_game.from_bytes(
162 | b"(;FF[4]AB[bc][cd]GM[1]HA[2]SZ[9];W[aa];B[ab])")
163 | sgf_moves.indicate_first_player(g8)
164 | tc.assertEqual(g8.serialise(),
165 | b"(;FF[4]AB[bc][cd]GM[1]HA[2]SZ[9];W[aa];B[ab])\n")
166 |
--------------------------------------------------------------------------------
/sgfmill_tests/sgf_properties_tests.py:
--------------------------------------------------------------------------------
1 | """Tests for sgf_properties.py."""
2 |
3 | from . import sgfmill_test_support
4 |
5 | from sgfmill import sgf_properties
6 |
7 | def make_tests(suite):
8 | suite.addTests(sgfmill_test_support.make_simple_tests(globals()))
9 |
10 | def test_interpret_simpletext(tc):
11 | def interpret(bb, encoding):
12 | context = sgf_properties._Context(19, encoding)
13 | return sgf_properties.interpret_simpletext(bb, context)
14 | tc.assertEqual(interpret(b"a\nb\\\\c", "utf-8"), "a b\\c")
15 | s = "test \N{POUND SIGN}"
16 | tc.assertEqual(interpret(s.encode("utf-8"), "UTF-8"), s)
17 | tc.assertEqual(interpret(s.encode("iso-8859-1"), "ISO-8859-1"), s)
18 | tc.assertRaises(UnicodeDecodeError, interpret,
19 | s.encode("iso-8859-1"), "UTF-8")
20 | tc.assertRaises(UnicodeDecodeError, interpret, s.encode("utf-8"), "ASCII")
21 |
22 | def test_serialise_simpletext(tc):
23 | def serialise(s, encoding):
24 | context = sgf_properties._Context(19, encoding)
25 | return sgf_properties.serialise_simpletext(s, context)
26 | tc.assertEqual(serialise("ab\\c", "utf-8"), b"ab\\\\c")
27 | s = "test \N{POUND SIGN}"
28 | tc.assertEqual(serialise(s, "UTF-8"), s.encode("utf-8"))
29 | tc.assertEqual(serialise(s, "ISO-8859-1"), s.encode("iso-8859-1"))
30 | tc.assertRaises(UnicodeEncodeError, serialise, "\N{EN DASH}", "ISO-8859-1")
31 | tc.assertRaisesRegex(TypeError, "^expected string, given bytes$",
32 | serialise, b'test', "utf-8")
33 |
34 | def test_interpret_text(tc):
35 | def interpret(bb, encoding):
36 | context = sgf_properties._Context(19, encoding)
37 | return sgf_properties.interpret_text(bb, context)
38 | tc.assertEqual(interpret(b"a\nb\\\\c", "utf-8"), "a\nb\\c")
39 | s = "test \N{POUND SIGN}"
40 | tc.assertEqual(interpret(s.encode("utf-8"), "UTF-8"), s)
41 | tc.assertEqual(interpret(s.encode("iso-8859-1"), "ISO-8859-1"), s)
42 | tc.assertRaises(UnicodeDecodeError, interpret,
43 | s.encode("iso-8859-1"), "UTF-8")
44 | tc.assertRaises(UnicodeDecodeError, interpret, s.encode("utf-8"), "ASCII")
45 |
46 | def test_serialise_text(tc):
47 | def serialise(s, encoding):
48 | context = sgf_properties._Context(19, encoding)
49 | return sgf_properties.serialise_text(s, context)
50 | tc.assertEqual(serialise("ab\\c", "utf-8"), b"ab\\\\c")
51 | s = "test \N{POUND SIGN}"
52 | tc.assertEqual(serialise(s, "UTF-8"), s.encode("utf-8"))
53 | tc.assertEqual(serialise(s, "ISO-8859-1"), s.encode("iso-8859-1"))
54 | tc.assertRaises(UnicodeEncodeError, serialise, "\N{EN DASH}", "ISO-8859-1")
55 | tc.assertRaisesRegex(TypeError, "^expected string, given bytes$",
56 | serialise, b'test', "utf-8")
57 |
58 | def test_interpret_none(tc):
59 | interpret_none = sgf_properties.interpret_none
60 | tc.assertIs(interpret_none(b""), True)
61 | tc.assertIs(interpret_none(b"xxx"), True)
62 |
63 | def test_serialise_none(tc):
64 | serialise_none = sgf_properties.serialise_none
65 | tc.assertEqual(serialise_none(None), b"")
66 | tc.assertEqual(serialise_none(1), b"")
67 | tc.assertEqual(serialise_none("x"), b"")
68 |
69 | def test_interpret_number(tc):
70 | interpret_number = sgf_properties.interpret_number
71 | tc.assertEqual(interpret_number(b"1"), 1)
72 | tc.assertIs(type(interpret_number(b"1")), int)
73 | tc.assertEqual(interpret_number(b"0"), 0)
74 | tc.assertEqual(interpret_number(b"-1"), -1)
75 | tc.assertEqual(interpret_number(b"+1"), 1)
76 | tc.assertEqual(interpret_number(b" 3"), 3)
77 | tc.assertEqual(interpret_number(b"4\n"), 4)
78 | tc.assertEqual(interpret_number(b"+ 1"), 1)
79 | tc.assertEqual(interpret_number(b"- 1"), -1)
80 | tc.assertEqual(interpret_number(b"1 2"), 12)
81 | # Python 3.6 or later will accept this
82 | #tc.assertEqual(interpret_number(b"1_2"), 12)
83 | tc.assertRaises(ValueError, interpret_number, b"1.5")
84 | tc.assertRaises(ValueError, interpret_number, b"0xaf")
85 | tc.assertRaises(Exception, interpret_number, 1)
86 | #tc.assertRaises(TypeError, interpret_number, "1")
87 |
88 | def test_serialise_number(tc):
89 | serialise_number = sgf_properties.serialise_number
90 | tc.assertEqual(serialise_number(0), b"0")
91 | tc.assertEqual(serialise_number(1), b"1")
92 | tc.assertEqual(serialise_number(2), b"2")
93 | tc.assertEqual(serialise_number(2.0), b"2")
94 | tc.assertEqual(serialise_number(2.5), b"2")
95 | tc.assertRaises(TypeError, serialise_number, "1")
96 |
97 | def test_interpret_real(tc):
98 | interpret_real = sgf_properties.interpret_real
99 | tc.assertEqual(interpret_real(b"1"), 1.0)
100 | tc.assertIs(type(interpret_real(b"1")), float)
101 | tc.assertEqual(interpret_real(b"0"), 0.0)
102 | tc.assertEqual(interpret_real(b"1.0"), 1.0)
103 | tc.assertEqual(interpret_real(b"1.5"), 1.5)
104 | tc.assertEqual(interpret_real(b"-1.5"), -1.5)
105 | tc.assertEqual(interpret_real(b"+0.5"), 0.5)
106 | tc.assertRaises(ValueError, interpret_real, b"+")
107 | tc.assertRaises(ValueError, interpret_real, b"0xaf")
108 | tc.assertRaises(ValueError, interpret_real, b"inf")
109 | tc.assertRaises(ValueError, interpret_real, b"-inf")
110 | tc.assertRaises(ValueError, interpret_real, b"NaN")
111 | tc.assertRaises(ValueError, interpret_real, b"1e400")
112 | tc.assertRaises(TypeError, interpret_real, None)
113 | #tc.assertRaises(TypeError, interpret_real, "1.0")
114 | #tc.assertRaises(TypeError, interpret_real, 1.0)
115 |
116 | def test_serialise_real(tc):
117 | serialise_real = sgf_properties.serialise_real
118 | tc.assertEqual(serialise_real(1), b"1")
119 | tc.assertEqual(serialise_real(-1), b"-1")
120 | tc.assertEqual(serialise_real(1.0), b"1")
121 | tc.assertEqual(serialise_real(-1.0), b"-1")
122 | tc.assertEqual(serialise_real(1.5), b"1.5")
123 | tc.assertEqual(serialise_real(-1.5), b"-1.5")
124 | tc.assertEqual(serialise_real(0.001), b"0.001")
125 | tc.assertEqual(serialise_real(0.0001), b"0.0001")
126 | tc.assertEqual(serialise_real(0.00001), b"0")
127 | tc.assertEqual(serialise_real(1e15), b"1000000000000000")
128 | tc.assertEqual(serialise_real(1e16), b"10000000000000000")
129 | tc.assertEqual(serialise_real(1e17), b"100000000000000000")
130 | tc.assertEqual(serialise_real(1e18), b"1000000000000000000")
131 | tc.assertEqual(serialise_real(-1e18), b"-1000000000000000000")
132 | # 1e400 is inf
133 | tc.assertRaises(ValueError, serialise_real, 1e400)
134 | tc.assertRaises(ValueError, serialise_real, float("NaN"))
135 |
136 | def test_interpret_double(tc):
137 | interpret_double = sgf_properties.interpret_double
138 | tc.assertEqual(interpret_double(b"1"), 1)
139 | tc.assertEqual(interpret_double(b"2"), 2)
140 | tc.assertEqual(interpret_double(b"x"), 1)
141 | tc.assertEqual(interpret_double(b""), 1)
142 |
143 | def test_serialise_double(tc):
144 | serialise_double = sgf_properties.serialise_double
145 | tc.assertEqual(serialise_double(1), b"1")
146 | tc.assertEqual(serialise_double(2), b"2")
147 | tc.assertEqual(serialise_double(0), b"1")
148 | tc.assertEqual(serialise_double(3), b"1")
149 |
150 | def test_interpret_colour(tc):
151 | interpret_colour = sgf_properties.interpret_colour
152 | tc.assertEqual(interpret_colour(b"b"), "b")
153 | tc.assertEqual(interpret_colour(b"w"), "w")
154 | tc.assertEqual(interpret_colour(b"B"), "b")
155 | tc.assertEqual(interpret_colour(b"W"), "w")
156 | tc.assertRaises(ValueError, interpret_colour, b"")
157 | tc.assertRaises(ValueError, interpret_colour, b"x")
158 |
159 | def test_serialise_colour(tc):
160 | serialise_colour = sgf_properties.serialise_colour
161 | tc.assertEqual(serialise_colour('b'), b"B")
162 | tc.assertEqual(serialise_colour('w'), b"W")
163 | tc.assertRaises(ValueError, serialise_colour, "")
164 | tc.assertRaises(ValueError, serialise_colour, "x")
165 | tc.assertRaises(ValueError, serialise_colour, "B")
166 | tc.assertRaises(ValueError, serialise_colour, "W")
167 |
168 | def test_interpret_move(tc):
169 | def interpret_move(s, size):
170 | context = sgf_properties._Context(size, "UTF-8")
171 | return sgf_properties.interpret_move(s, context)
172 | tc.assertEqual(interpret_move(b"aa", 19), (18, 0))
173 | tc.assertEqual(interpret_move(b"ai", 19), (10, 0))
174 | tc.assertEqual(interpret_move(b"ba", 9), (8, 1))
175 | tc.assertEqual(interpret_move(b"tt", 21), (1, 19))
176 | tc.assertIs(interpret_move(b"tt", 19), None)
177 | tc.assertIs(interpret_move(b"", 19), None)
178 | tc.assertIs(interpret_move(b"", 21), None)
179 | tc.assertRaises(ValueError, interpret_move, b"Aa", 19)
180 | tc.assertRaises(ValueError, interpret_move, b"aA", 19)
181 | tc.assertRaises(ValueError, interpret_move, b"aaa", 19)
182 | tc.assertRaises(ValueError, interpret_move, b"a", 19)
183 | tc.assertRaises(ValueError, interpret_move, b"au", 19)
184 | tc.assertRaises(ValueError, interpret_move, b"ua", 19)
185 | tc.assertRaises(ValueError, interpret_move, b"a`", 19)
186 | tc.assertRaises(ValueError, interpret_move, b"`a", 19)
187 | tc.assertRaises(ValueError, interpret_move, b"11", 19)
188 | tc.assertRaises(ValueError, interpret_move, b" aa", 19)
189 | tc.assertRaises(ValueError, interpret_move, b"aa\x00", 19)
190 | tc.assertRaises(TypeError, interpret_move, None, 19)
191 | tc.assertRaises(TypeError, interpret_move, "aa", 19)
192 | tc.assertRaises(TypeError, interpret_move, (b'a', b'a'), 19)
193 | # tc.assertRaises(TypeError, interpret_move, (97, 97), 19)
194 |
195 | def test_serialise_move(tc):
196 | def serialise_move(s, size):
197 | context = sgf_properties._Context(size, "UTF-8")
198 | return sgf_properties.serialise_move(s, context)
199 | tc.assertEqual(serialise_move((18, 0), 19), b"aa")
200 | tc.assertEqual(serialise_move((10, 0), 19), b"ai")
201 | tc.assertEqual(serialise_move((8, 1), 19), b"bk")
202 | tc.assertEqual(serialise_move((8, 1), 9), b"ba")
203 | tc.assertEqual(serialise_move((1, 19), 21), b"tt")
204 | tc.assertEqual(serialise_move(None, 19), b"tt")
205 | tc.assertEqual(serialise_move(None, 20), b"")
206 | tc.assertRaises(ValueError, serialise_move, (3, 3), 0)
207 | tc.assertRaises(ValueError, serialise_move, (3, 3), 27)
208 | tc.assertRaises(ValueError, serialise_move, (9, 0), 9)
209 | tc.assertRaises(ValueError, serialise_move, (-1, 0), 9)
210 | tc.assertRaises(ValueError, serialise_move, (0, 9), 9)
211 | tc.assertRaises(ValueError, serialise_move, (0, -1), 9)
212 | tc.assertRaises(TypeError, serialise_move, (1, 1.5), 9)
213 |
214 | def test_interpret_point(tc):
215 | def interpret_point(s, size):
216 | context = sgf_properties._Context(size, "UTF-8")
217 | return sgf_properties.interpret_point(s, context)
218 | tc.assertEqual(interpret_point(b"aa", 19), (18, 0))
219 | tc.assertEqual(interpret_point(b"ai", 19), (10, 0))
220 | tc.assertEqual(interpret_point(b"ba", 9), (8, 1))
221 | tc.assertEqual(interpret_point(b"tt", 21), (1, 19))
222 | tc.assertRaises(ValueError, interpret_point, b"tt", 19)
223 | tc.assertRaises(ValueError, interpret_point, b"", 19)
224 | tc.assertRaises(ValueError, interpret_point, b"", 21)
225 | tc.assertRaises(ValueError, interpret_point, b"Aa", 19)
226 | tc.assertRaises(ValueError, interpret_point, b"aA", 19)
227 | tc.assertRaises(ValueError, interpret_point, b"aaa", 19)
228 | tc.assertRaises(ValueError, interpret_point, b"a", 19)
229 | tc.assertRaises(ValueError, interpret_point, b"au", 19)
230 | tc.assertRaises(ValueError, interpret_point, b"ua", 19)
231 | tc.assertRaises(ValueError, interpret_point, b"a`", 19)
232 | tc.assertRaises(ValueError, interpret_point, b"`a", 19)
233 | tc.assertRaises(ValueError, interpret_point, b"11", 19)
234 | tc.assertRaises(ValueError, interpret_point, b" aa", 19)
235 | tc.assertRaises(ValueError, interpret_point, b"aa\x00", 19)
236 | tc.assertRaises(TypeError, interpret_point, None, 19)
237 | tc.assertRaises(TypeError, interpret_point, (b'a', b'a'), 19)
238 | # tc.assertRaises(TypeError, interpret_point, (97, 97), 19)
239 |
240 | def test_serialise_point(tc):
241 | def serialise_point(s, size):
242 | context = sgf_properties._Context(size, "UTF-8")
243 | return sgf_properties.serialise_point(s, context)
244 | tc.assertEqual(serialise_point((18, 0), 19), b"aa")
245 | tc.assertEqual(serialise_point((10, 0), 19), b"ai")
246 | tc.assertEqual(serialise_point((8, 1), 19), b"bk")
247 | tc.assertEqual(serialise_point((8, 1), 9), b"ba")
248 | tc.assertEqual(serialise_point((1, 19), 21), b"tt")
249 | tc.assertRaises(ValueError, serialise_point, None, 19)
250 | tc.assertRaises(ValueError, serialise_point, None, 20)
251 | tc.assertRaises(ValueError, serialise_point, (3, 3), 0)
252 | tc.assertRaises(ValueError, serialise_point, (3, 3), 27)
253 | tc.assertRaises(ValueError, serialise_point, (9, 0), 9)
254 | tc.assertRaises(ValueError, serialise_point, (-1, 0), 9)
255 | tc.assertRaises(ValueError, serialise_point, (0, 9), 9)
256 | tc.assertRaises(ValueError, serialise_point, (0, -1), 9)
257 | tc.assertRaises(TypeError, serialise_point, (1, 1.5), 9)
258 |
259 |
260 | def test_interpret_point_list(tc):
261 | def ipl(l, size):
262 | context = sgf_properties._Context(size, "UTF-8")
263 | return sgf_properties.interpret_point_list(l, context)
264 | tc.assertEqual(ipl([], 19),
265 | set())
266 | tc.assertEqual(ipl([b"aa"], 19),
267 | {(18, 0)})
268 | tc.assertEqual(ipl([b"aa", b"ai"], 19),
269 | {(18, 0), (10, 0)})
270 | tc.assertEqual(ipl([b"ab:bc"], 19),
271 | {(16, 0), (16, 1), (17, 0), (17, 1)})
272 | tc.assertEqual(ipl([b"ab:bc", b"aa"], 19),
273 | {(18, 0), (16, 0), (16, 1), (17, 0), (17, 1)})
274 | # overlap is forbidden by the spec, but we accept it
275 | tc.assertEqual(ipl([b"aa", b"aa"], 19),
276 | {(18, 0)})
277 | tc.assertEqual(ipl([b"ab:bc", b"bb:bc"], 19),
278 | {(16, 0), (16, 1), (17, 0), (17, 1)})
279 | # 1x1 rectangles are forbidden by the spec, but we accept them
280 | tc.assertEqual(ipl([b"aa", b"bb:bb"], 19),
281 | {(18, 0), (17, 1)})
282 | # 'backwards' rectangles are forbidden by the spec, and we reject them
283 | tc.assertRaises(ValueError, ipl, [b"ab:aa"], 19)
284 | tc.assertRaises(ValueError, ipl, [b"ba:aa"], 19)
285 | tc.assertRaises(ValueError, ipl, [b"bb:aa"], 19)
286 |
287 | tc.assertRaises(ValueError, ipl, [b"aa", b"tt"], 19)
288 | tc.assertRaises(ValueError, ipl, [b"aa", b""], 19)
289 | tc.assertRaises(ValueError, ipl, [b"aa:", b"aa"], 19)
290 | tc.assertRaises(ValueError, ipl, [b"aa:tt", b"aa"], 19)
291 | tc.assertRaises(ValueError, ipl, [b"tt:aa", b"aa"], 19)
292 |
293 | def test_compressed_point_list_spec_example(tc):
294 | # Checks the examples at http://www.red-bean.com/sgf/DD_VW.html
295 | def sgf_point(move, size):
296 | row, col = move
297 | row = size - row - 1
298 | col_s = b"abcdefghijklmnopqrstuvwxy"[col]
299 | row_s = b"abcdefghijklmnopqrstuvwxy"[row]
300 | return bytes((col_s, row_s))
301 |
302 | def ipl(l, size):
303 | context = sgf_properties._Context(size, "UTF-8")
304 | return sgf_properties.interpret_point_list(l, context)
305 | tc.assertEqual(
306 | set(sgf_point(move, 9) for move in ipl([b"ac:ic"], 9)),
307 | {b"ac", b"bc", b"cc", b"dc", b"ec", b"fc", b"gc", b"hc", b"ic"})
308 | tc.assertEqual(
309 | set(sgf_point(move, 9) for move in ipl([b"ae:ie"], 9)),
310 | {b"ae", b"be", b"ce", b"de", b"ee", b"fe", b"ge", b"he", b"ie"})
311 | tc.assertEqual(
312 | set(sgf_point(move, 9) for move in ipl([b"aa:bi", b"ca:ce"], 9)),
313 | {b"aa", b"ab", b"ac", b"ad", b"ae", b"af", b"ag", b"ah", b"ai",
314 | b"bi", b"bh", b"bg", b"bf", b"be", b"bd", b"bc", b"bb", b"ba",
315 | b"ca", b"cb", b"cc", b"cd", b"ce"})
316 |
317 | def test_serialise_point_list(tc):
318 | def ipl(l, size):
319 | context = sgf_properties._Context(size, "UTF-8")
320 | return sgf_properties.interpret_point_list(l, context)
321 | def spl(l, size):
322 | context = sgf_properties._Context(size, "UTF-8")
323 | return sgf_properties.serialise_point_list(l, context)
324 |
325 | tc.assertEqual(spl([(18, 0), (17, 1)], 19), [b'aa', b'bb'])
326 | tc.assertEqual(spl([(17, 1), (18, 0)], 19), [b'aa', b'bb'])
327 | tc.assertEqual(spl([], 9), [])
328 | tc.assertEqual(ipl(spl([(1,2), (3,4), (4,5)], 19), 19),
329 | {(1,2), (3,4), (4,5)})
330 | tc.assertRaises(ValueError, spl, [(18, 0), None], 19)
331 |
332 |
333 | def test_AP(tc):
334 | def serialise(arg):
335 | context = sgf_properties._Context(19, "UTF-8")
336 | return sgf_properties.serialise_AP(arg, context)
337 | def interpret(arg):
338 | context = sgf_properties._Context(19, "UTF-8")
339 | return sgf_properties.interpret_AP(arg, context)
340 |
341 | tc.assertEqual(serialise(("foo:bar", "2\n3")), b"foo\\:bar:2\n3")
342 | tc.assertEqual(interpret(b"foo\\:bar:2 3"), ("foo:bar", "2 3"))
343 | tc.assertEqual(interpret(b"foo bar"), ("foo bar", ""))
344 |
345 | def test_ARLN(tc):
346 | def serialise(arg, size):
347 | context = sgf_properties._Context(size, "UTF-8")
348 | return sgf_properties.serialise_ARLN_list(arg, context)
349 | def interpret(arg, size):
350 | context = sgf_properties._Context(size, "UTF-8")
351 | return sgf_properties.interpret_ARLN_list(arg, context)
352 |
353 | tc.assertEqual(serialise([], 19), [])
354 | tc.assertEqual(interpret([], 19), [])
355 | tc.assertEqual(serialise([((7, 0), (5, 2)), ((4, 3), (2, 5))], 9),
356 | [b'ab:cd', b'de:fg'])
357 | tc.assertEqual(interpret([b'ab:cd', b'de:fg'], 9),
358 | [((7, 0), (5, 2)), ((4, 3), (2, 5))])
359 | tc.assertRaises(ValueError, serialise, [((7, 0), None)], 9)
360 | tc.assertRaises(ValueError, interpret, [b'ab:tt', b'de:fg'], 9)
361 |
362 | def test_FG(tc):
363 | def serialise(arg):
364 | context = sgf_properties._Context(19, "UTF-8")
365 | return sgf_properties.serialise_FG(arg, context)
366 | def interpret(arg):
367 | context = sgf_properties._Context(19, "UTF-8")
368 | return sgf_properties.interpret_FG(arg, context)
369 | tc.assertEqual(serialise(None), b"")
370 | tc.assertEqual(interpret(b""), None)
371 | tc.assertEqual(serialise((515, "th]is")), b"515:th\\]is")
372 | tc.assertEqual(interpret(b"515:th\\]is"), (515, "th]is"))
373 |
374 | def test_LB(tc):
375 | def serialise(arg, size):
376 | context = sgf_properties._Context(size, "UTF-8")
377 | return sgf_properties.serialise_LB_list(arg, context)
378 | def interpret(arg, size):
379 | context = sgf_properties._Context(size, "UTF-8")
380 | return sgf_properties.interpret_LB_list(arg, context)
381 | tc.assertEqual(serialise([], 19), [])
382 | tc.assertEqual(interpret([], 19), [])
383 | tc.assertEqual(
384 | serialise([((6, 0), "lbl"), ((6, 1), "lb]l2")], 9),
385 | [b"ac:lbl", b"bc:lb\\]l2"])
386 | tc.assertEqual(
387 | interpret([b"ac:lbl", b"bc:lb\\]l2"], 9),
388 | [((6, 0), "lbl"), ((6, 1), "lb]l2")])
389 | tc.assertRaises(ValueError, serialise, [(None, "lbl")], 9)
390 | tc.assertRaises(ValueError, interpret, [b':lbl', b'de:lbl2'], 9)
391 |
392 |
393 | def test_presenter_interpret(tc):
394 | p9 = sgf_properties.Presenter(9, "UTF-8")
395 | p19 = sgf_properties.Presenter(19, "UTF-8")
396 | tc.assertEqual(p9.interpret('KO', [b""]), True)
397 | tc.assertEqual(p9.interpret('SZ', [b"9"]), 9)
398 | tc.assertEqual(p9.interpret('B', [b"ca"]), (8, 2))
399 | tc.assertEqual(p19.interpret('B', [b"ca"]), (18, 2))
400 | tc.assertRaisesRegex(ValueError, "multiple values",
401 | p9.interpret, 'SZ', [b"9", b"blah"])
402 | tc.assertEqual(p9.interpret('CR', [b"ab", b"cd"]), {(5, 2), (7, 0)})
403 | tc.assertRaises(ValueError, p9.interpret, 'SZ', [])
404 | tc.assertRaises(ValueError, p9.interpret, 'CR', [])
405 | tc.assertEqual(p9.interpret('DD', [b""]), set())
406 | # all lists are treated like elists
407 | tc.assertEqual(p9.interpret('CR', [b""]), set())
408 |
409 | def test_presenter_serialise(tc):
410 | p9 = sgf_properties.Presenter(9, "UTF-8")
411 | p19 = sgf_properties.Presenter(19, "UTF-8")
412 |
413 | tc.assertEqual(p9.serialise('KO', True), [b""])
414 | tc.assertEqual(p9.serialise('SZ', 9), [b"9"])
415 | tc.assertEqual(p9.serialise('KM', 3.5), [b"3.5"])
416 | tc.assertEqual(p9.serialise('C', "foo\\:b]ar\n"), [b"foo\\\\:b\\]ar\n"])
417 | tc.assertEqual(p9.serialise('B', (8, 2)), [b"ca"])
418 | tc.assertEqual(p19.serialise('B', (18, 2)), [b"ca"])
419 | tc.assertEqual(p9.serialise('B', None), [b"tt"])
420 | tc.assertEqual(p19.serialise('AW', {(17, 1), (18, 0)}), [b"aa", b"bb"])
421 | tc.assertEqual(p9.serialise('DD', [(1, 2), (3, 4)]), [b"ch", b"ef"])
422 | tc.assertEqual(p9.serialise('DD', []), [b""])
423 | tc.assertRaisesRegex(ValueError, "empty list", p9.serialise, 'CR', [])
424 | tc.assertEqual(p9.serialise('AP', ("na:me", "2.3")), [b"na\\:me:2.3"])
425 | tc.assertEqual(p9.serialise('FG', (515, "th]is")), [b"515:th\\]is"])
426 | tc.assertEqual(p9.serialise('XX', "foo\\bar"), [b"foo\\\\bar"])
427 |
428 | tc.assertRaises(ValueError, p9.serialise, 'B', (1, 9))
429 |
430 | def test_presenter_private_properties(tc):
431 | p9 = sgf_properties.Presenter(9, "UTF-8")
432 | tc.assertEqual(p9.serialise('XX', "9"), [b"9"])
433 | tc.assertEqual(p9.interpret('XX', [b"9"]), "9")
434 | p9.set_private_property_type(p9.get_property_type("SZ"))
435 | tc.assertEqual(p9.serialise('XX', 9), [b"9"])
436 | tc.assertEqual(p9.interpret('XX', [b"9"]), 9)
437 | p9.set_private_property_type(None)
438 | tc.assertRaisesRegex(ValueError, "unknown property",
439 | p9.serialise, 'XX', "foo\\bar")
440 | tc.assertRaisesRegex(ValueError, "unknown property",
441 | p9.interpret, 'XX', [b"asd"])
442 |
443 |
--------------------------------------------------------------------------------
/sgfmill_tests/sgfmill_test_support.py:
--------------------------------------------------------------------------------
1 | """Sgfmill-specific test support code."""
2 |
3 | import re
4 | import textwrap
5 | import unittest
6 |
7 | from . import test_framework
8 |
9 | from sgfmill.common import format_vertex
10 | from sgfmill import ascii_boards
11 | from sgfmill import boards
12 |
13 | # This makes TestResult ignore lines from this module in tracebacks
14 | __unittest = True
15 |
16 |
17 | def dedent(v):
18 | """Variant of textwrap.dedent which also accepts bytes."""
19 | if isinstance(v, bytes):
20 | return textwrap.dedent(v.decode("iso-8859-1")).encode("iso-8859-1")
21 | else:
22 | return textwrap.dedent(v)
23 |
24 |
25 | def compare_boards(b1, b2):
26 | """Check whether two boards have the same position.
27 |
28 | returns a pair (position_is_the_same, message)
29 |
30 | """
31 | if b1.side != b2.side:
32 | raise ValueError("size is different: %s, %s" % (b1.side, b2.side))
33 | differences = []
34 | for row, col in b1.board_points:
35 | if b1.get(row, col) != b2.get(row, col):
36 | differences.append((row, col))
37 | if not differences:
38 | return True, None
39 | msg = "boards differ at %s" % " ".join(map(format_vertex, differences))
40 | try:
41 | msg += "\n%s\n%s" % (
42 | ascii_boards.render_board(b1), ascii_boards.render_board(b2))
43 | except Exception:
44 | pass
45 | return False, msg
46 |
47 | def compare_boards_or_diagrams(b1, b2):
48 | """Variant of compare_boards which allows diagrams too.
49 |
50 | returns a pair (position_is_the_same, message)
51 |
52 | Compares as boards if the diagram can be interpreted; otherwise renders the
53 | board and compares as strings.
54 |
55 | If given two diagrams, compares them as strings.
56 |
57 | Note that board comparision is more lenient than string comparison, to
58 | whatever extent interpret_diagram() is lenient (in particular it accepts
59 | leading and trailing whitespace).
60 |
61 | """
62 | def coerce(board, diagram):
63 | try:
64 | return board, ascii_boards.interpret_diagram(diagram, board.side)
65 | except ValueError:
66 | return ascii_boards.render_board(board), diagram
67 | if isinstance(b1, boards.Board) and isinstance(b2, str):
68 | b1, b2 = coerce(b1, b2)
69 | elif isinstance(b2, boards.Board) and isinstance(b1, str):
70 | b2, b1 = coerce(b2, b1)
71 | if isinstance(b1, boards.Board):
72 | return compare_boards(b1, b2)
73 | else:
74 | return compare_diagrams(b1, b2)
75 |
76 | def compare_diagrams(d1, d2):
77 | """Compare two ascii board diagrams.
78 |
79 | returns a pair (strings_are_equal, message)
80 |
81 | (assertMultiLineEqual tends to look nasty for these, so we just show them
82 | both in full)
83 |
84 | """
85 | if d1 == d2:
86 | return True, None
87 | return False, "diagrams differ:\n%s\n\n%s" % (d1, d2)
88 |
89 | class Sgfmill_testcase_mixin:
90 | """TestCase mixin adding support for sgfmill-specific types.
91 |
92 | Board/diagram features:
93 | assertBoardEqual
94 | assertDiagramEqual
95 | assertEqual and assertNotEqual for Boards
96 |
97 | """
98 | def init_sgfmill_testcase_mixin(self):
99 | self.addTypeEqualityFunc(boards.Board, self.assertBoardEqual)
100 |
101 | def _format_message(self, msg, standardMsg):
102 | # This is the same as _formatMessage from python 3.4 unittest; copying
103 | # it because it's not part of the public API.
104 | if not self.longMessage:
105 | return msg or standardMsg
106 | if msg is None:
107 | return standardMsg
108 | try:
109 | return '%s : %s' % (standardMsg, msg)
110 | except UnicodeDecodeError:
111 | return '%s : %s' % (unittest.util.safe_repr(standardMsg),
112 | unittest.util.safe_repr(msg))
113 |
114 | def assertBoardEqual(self, b1, b2, msg=None):
115 | """assertEqual for two boards.
116 |
117 | Accepts diagrams too; see compare_boards_or_diagrams.
118 |
119 | """
120 | are_equal, desc = compare_boards_or_diagrams(b1, b2)
121 | if not are_equal:
122 | self.fail(self._format_message(msg, desc+"\n"))
123 |
124 | def assertDiagramEqual(self, d1, d2, msg=None):
125 | """Variant of assertMultiLineEqual for board diagrams.
126 |
127 | Checks that two strings are equal, with difference reporting
128 | appropriate for board diagrams.
129 |
130 | """
131 | are_equal, desc = compare_diagrams(d1, d2)
132 | if not are_equal:
133 | self.fail(self._format_message(msg, desc+"\n"))
134 |
135 | def assertNotEqual(self, first, second, msg=None):
136 | if isinstance(first, boards.Board) and isinstance(second, boards.Board):
137 | are_equal, _ = compare_boards(first, second)
138 | if not are_equal:
139 | return
140 | msg = self._format_message(msg, 'boards have the same position')
141 | raise self.failureException(msg)
142 | super().assertNotEqual(first, second, msg)
143 |
144 |
145 | class Sgfmill_SimpleTestCase(Sgfmill_testcase_mixin,
146 | test_framework.SimpleTestCase):
147 | """SimpleTestCase with the Sgfmill mixin."""
148 | def __init__(self, *args, **kwargs):
149 | super().__init__(*args, **kwargs)
150 | self.init_sgfmill_testcase_mixin()
151 |
152 | class Sgfmill_ParameterisedTestCase(Sgfmill_testcase_mixin,
153 | test_framework.ParameterisedTestCase):
154 | """ParameterisedTestCase with the Sgfmill mixin."""
155 | def __init__(self, *args, **kwargs):
156 | super().__init__(*args, **kwargs)
157 | self.init_sgfmill_testcase_mixin()
158 |
159 |
160 | def make_simple_tests(source, prefix="test_"):
161 | """Make test cases from a module's test_xxx functions.
162 |
163 | See test_framework for details.
164 |
165 | The test functions can use the Sgfmill_testcase_mixin enhancements.
166 |
167 | """
168 | return test_framework.make_simple_tests(
169 | source, prefix, testcase_class=Sgfmill_SimpleTestCase)
170 |
--------------------------------------------------------------------------------
/sgfmill_tests/test_framework.py:
--------------------------------------------------------------------------------
1 | """Generic (non-sgfmill-specific) test framework code."""
2 |
3 | import sys
4 | import unittest
5 |
6 | # This makes TestResult ignore lines from this module in tracebacks
7 | __unittest = True
8 |
9 | class SupporterError(Exception):
10 | """Exception raised by support objects when something goes wrong.
11 |
12 | This is raised to indicate things like sequencing errors detected by mock
13 | objects.
14 |
15 | """
16 |
17 | class SimpleTestCase(unittest.TestCase):
18 | """TestCase which runs a single function.
19 |
20 | Instantiate with the test function, which takes a TestCase parameter, eg:
21 | def test_xxx(tc):
22 | tc.assertEqual(2+2, 4)
23 |
24 | """
25 |
26 | def __init__(self, fn):
27 | super().__init__()
28 | self.fn = fn
29 | try:
30 | self.name = fn.__module__.split(".", 1)[-1] + "." + fn.__name__
31 | except AttributeError:
32 | self.name = str(fn)
33 |
34 | def runTest(self):
35 | self.fn(self)
36 |
37 | def id(self):
38 | return self.name
39 |
40 | def shortDescription(self):
41 | return None
42 |
43 | def __str__(self):
44 | return self.name
45 |
46 | def __repr__(self):
47 | return "" % self.name
48 |
49 |
50 | class ParameterisedTestCase(unittest.TestCase):
51 | """Parameterised testcase.
52 |
53 | Subclasses should define:
54 | test_name -- short string
55 | parameter_names -- list of identifiers
56 | runTest
57 |
58 | """
59 | def __init__(self, code, *parameters):
60 | super().__init__()
61 | self.code = code
62 | self.name = "%s.%s:%s" % (self.__class__.__module__.split(".", 1)[-1],
63 | self.test_name, code)
64 | for name, value in zip(self.parameter_names, parameters):
65 | setattr(self, name, value)
66 |
67 | def runTest(self):
68 | raise NotImplementedError
69 |
70 | def id(self):
71 | return self.name
72 |
73 | def shortDescription(self):
74 | return None
75 |
76 | def __str__(self):
77 | return self.name
78 |
79 | def __repr__(self):
80 | return "<%s: %s>" % (self.__class__.__name__, self.name)
81 |
82 |
83 |
84 | def _function_sort_key(fn):
85 | try:
86 | return fn.__code__.co_firstlineno
87 | except AttributeError:
88 | return str(fn)
89 |
90 | def make_simple_tests(source, prefix="test_", testcase_class=SimpleTestCase):
91 | """Make test cases from a module's test_xxx functions.
92 |
93 | source -- dict (usually a module's globals()).
94 | prefix -- string (default "test_")
95 | testcase_class -- SimpleTestCase subclass to use
96 |
97 | Returns a list of TestCase objects.
98 |
99 | This makes a TestCase for each function in the values of 'source' whose
100 | name begins with 'prefix'.
101 |
102 | The list is in the order of function definition (using the line number
103 | attribute).
104 |
105 | """
106 | functions = [value for name, value in source.items()
107 | if name.startswith(prefix) and callable(value)]
108 | functions.sort(key=_function_sort_key)
109 | return [testcase_class(fn) for fn in functions]
110 |
111 |
--------------------------------------------------------------------------------
/test_installed_sgfmill.py:
--------------------------------------------------------------------------------
1 | """Run the sgfmill testsuite against an installed sgfmill package."""
2 |
3 | import imp
4 | import os
5 | import sys
6 |
7 | from pathlib import Path
8 |
9 | project_dir = Path(Path.cwd(), __file__).parent
10 |
11 | # Remove the distribution directory from sys.path
12 | if os.path.abspath(sys.path[0]) == str(project_dir):
13 | del sys.path[0]
14 |
15 | try:
16 | import sgfmill
17 | except ImportError:
18 | sys.exit("test_installed_sgfmill: can't find the sgfmill package")
19 |
20 | PACKAGE_NAME = "sgfmill_tests"
21 |
22 | # Make sgfmill_tests importable without the sibling sgfmill
23 | packagepath = Path(project_dir, PACKAGE_NAME)
24 | mdl = imp.load_package(PACKAGE_NAME, str(packagepath))
25 | sys.modules[PACKAGE_NAME] = mdl
26 |
27 | found = Path(Path.cwd(), sgfmill.__file__).parent
28 | print("testing sgfmill package in %s" % found, file=sys.stderr)
29 | from sgfmill_tests import run_sgfmill_testsuite
30 | run_sgfmill_testsuite.run(sys.argv[1:])
31 |
32 |
--------------------------------------------------------------------------------