├── .editorconfig
├── .githooks
└── pre-commit
├── .github
└── workflows
│ ├── codeql-analysis.yml
│ └── pythonpackage.yml
├── .gitignore
├── LICENSE
├── Makefile
├── README.rst
├── data
└── games
│ └── habu-fujii-2006.kif
├── pyproject.toml
├── requirements.txt
├── scripts
├── random_csa_tcp_match
├── random_self_match
└── random_shogidokoro_match
├── shogi
├── CSA.py
├── Consts.py
├── KIF.py
├── Move.py
├── Person.py
├── Piece.py
└── __init__.py
└── tests
├── board_test.py
├── csa_test.py
├── kif_test.py
├── move_test.py
├── perft_test.py
└── person_test.py
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | insert_final_newline = true
6 | charset = utf-8
7 |
8 | [*.{py,ini}]
9 | indent_style = space
10 | indent_size = 4
11 | trim_trailing_whitespace = true
12 | insert_final_newline = false
13 | max_line_length = 120
14 |
15 | [scripts/*]
16 | indent_style = space
17 | indent_size = 4
18 | trim_trailing_whitespace = true
19 | insert_final_newline = false
20 | max_line_length = 120
21 |
22 | [tests/{kif,csa}_test.py]
23 | trim_trailing_whitespace = false
24 |
--------------------------------------------------------------------------------
/.githooks/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | make format
3 |
4 | for FILE in `git diff --cached --name-only --diff-filter=ACM`; do
5 | git add $FILE
6 | done
7 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | name: "CodeQL"
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 | schedule:
9 | - cron: '35 11 * * 4'
10 |
11 | jobs:
12 | analyze:
13 | name: Analyze
14 | runs-on: ubuntu-latest
15 | permissions:
16 | actions: read
17 | contents: read
18 | security-events: write
19 |
20 | strategy:
21 | fail-fast: false
22 | matrix:
23 | language: [ 'python' ]
24 |
25 | steps:
26 | - name: Checkout repository
27 | uses: actions/checkout@v2
28 |
29 | - name: Initialize CodeQL
30 | uses: github/codeql-action/init@v1
31 | with:
32 | languages: ${{ matrix.language }}
33 |
34 | - name: Autobuild
35 | uses: github/codeql-action/autobuild@v1
36 |
37 | - name: Perform CodeQL Analysis
38 | uses: github/codeql-action/analyze@v1
39 |
--------------------------------------------------------------------------------
/.github/workflows/pythonpackage.yml:
--------------------------------------------------------------------------------
1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
3 |
4 | name: Python package
5 |
6 | on:
7 | push:
8 | branches: [ master ]
9 | pull_request:
10 | branches: [ master ]
11 |
12 | jobs:
13 | build:
14 |
15 | runs-on: ubuntu-latest
16 | strategy:
17 | matrix:
18 | python-version: ["3.8", "3.9", "3.10", "3.11"]
19 |
20 | steps:
21 | - uses: actions/checkout@v2
22 | - name: Set up Python ${{ matrix.python-version }}
23 | uses: actions/setup-python@v1
24 | with:
25 | python-version: ${{ matrix.python-version }}
26 | - name: Install dependencies
27 | run: |
28 | python -m pip install --upgrade pip
29 | make install
30 | - name: Lint
31 | run: |
32 | make format
33 | - name: Test
34 | run: |
35 | make test
36 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.swp
2 | *.py[cod]
3 | *~
4 | .eggs/
5 | .coverage
6 | .coveralls.yml
7 | .python-version
8 | .tox/
9 | dist/
10 | build/
11 | temp/
12 | python_shogi.egg-info/
13 | nosetests.xml
14 | docs/_build/
15 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU General Public License is a free, copyleft license for
11 | software and other kinds of works.
12 |
13 | The licenses for most software and other practical works are designed
14 | to take away your freedom to share and change the works. By contrast,
15 | the GNU General Public License is intended to guarantee your freedom to
16 | share and change all versions of a program--to make sure it remains free
17 | software for all its users. We, the Free Software Foundation, use the
18 | GNU General Public License for most of our software; it applies also to
19 | any other work released this way by its authors. You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | them if you wish), that you receive source code or can get it if you
26 | want it, that you can change the software or use pieces of it in new
27 | free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you
30 | these rights or asking you to surrender the rights. Therefore, you have
31 | certain responsibilities if you distribute copies of the software, or if
32 | you modify it: responsibilities to respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must pass on to the recipients the same
36 | freedoms that you received. You must make sure that they, too, receive
37 | or can get the source code. And you must show them these terms so they
38 | know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps:
41 | (1) assert copyright on the software, and (2) offer you this License
42 | giving you legal permission to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains
45 | that there is no warranty for this free software. For both users' and
46 | authors' sake, the GPL requires that modified versions be marked as
47 | changed, so that their problems will not be attributed erroneously to
48 | authors of previous versions.
49 |
50 | Some devices are designed to deny users access to install or run
51 | modified versions of the software inside them, although the manufacturer
52 | can do so. This is fundamentally incompatible with the aim of
53 | protecting users' freedom to change the software. The systematic
54 | pattern of such abuse occurs in the area of products for individuals to
55 | use, which is precisely where it is most unacceptable. Therefore, we
56 | have designed this version of the GPL to prohibit the practice for those
57 | products. If such problems arise substantially in other domains, we
58 | stand ready to extend this provision to those domains in future versions
59 | of the GPL, as needed to protect the freedom of users.
60 |
61 | Finally, every program is threatened constantly by software patents.
62 | States should not allow patents to restrict development and use of
63 | software on general-purpose computers, but in those that do, we wish to
64 | avoid the special danger that patents applied to a free program could
65 | make it effectively proprietary. To prevent this, the GPL assures that
66 | patents cannot be used to render the program non-free.
67 |
68 | The precise terms and conditions for copying, distribution and
69 | modification follow.
70 |
71 | TERMS AND CONDITIONS
72 |
73 | 0. Definitions.
74 |
75 | "This License" refers to version 3 of the GNU General Public License.
76 |
77 | "Copyright" also means copyright-like laws that apply to other kinds of
78 | works, such as semiconductor masks.
79 |
80 | "The Program" refers to any copyrightable work licensed under this
81 | License. Each licensee is addressed as "you". "Licensees" and
82 | "recipients" may be individuals or organizations.
83 |
84 | To "modify" a work means to copy from or adapt all or part of the work
85 | in a fashion requiring copyright permission, other than the making of an
86 | exact copy. The resulting work is called a "modified version" of the
87 | earlier work or a work "based on" the earlier work.
88 |
89 | A "covered work" means either the unmodified Program or a work based
90 | on the Program.
91 |
92 | To "propagate" a work means to do anything with it that, without
93 | permission, would make you directly or secondarily liable for
94 | infringement under applicable copyright law, except executing it on a
95 | computer or modifying a private copy. Propagation includes copying,
96 | distribution (with or without modification), making available to the
97 | public, and in some countries other activities as well.
98 |
99 | To "convey" a work means any kind of propagation that enables other
100 | parties to make or receive copies. Mere interaction with a user through
101 | a computer network, with no transfer of a copy, is not conveying.
102 |
103 | An interactive user interface displays "Appropriate Legal Notices"
104 | to the extent that it includes a convenient and prominently visible
105 | feature that (1) displays an appropriate copyright notice, and (2)
106 | tells the user that there is no warranty for the work (except to the
107 | extent that warranties are provided), that licensees may convey the
108 | work under this License, and how to view a copy of this License. If
109 | the interface presents a list of user commands or options, such as a
110 | menu, a prominent item in the list meets this criterion.
111 |
112 | 1. Source Code.
113 |
114 | The "source code" for a work means the preferred form of the work
115 | for making modifications to it. "Object code" means any non-source
116 | form of a work.
117 |
118 | A "Standard Interface" means an interface that either is an official
119 | standard defined by a recognized standards body, or, in the case of
120 | interfaces specified for a particular programming language, one that
121 | is widely used among developers working in that language.
122 |
123 | The "System Libraries" of an executable work include anything, other
124 | than the work as a whole, that (a) is included in the normal form of
125 | packaging a Major Component, but which is not part of that Major
126 | Component, and (b) serves only to enable use of the work with that
127 | Major Component, or to implement a Standard Interface for which an
128 | implementation is available to the public in source code form. A
129 | "Major Component", in this context, means a major essential component
130 | (kernel, window system, and so on) of the specific operating system
131 | (if any) on which the executable work runs, or a compiler used to
132 | produce the work, or an object code interpreter used to run it.
133 |
134 | The "Corresponding Source" for a work in object code form means all
135 | the source code needed to generate, install, and (for an executable
136 | work) run the object code and to modify the work, including scripts to
137 | control those activities. However, it does not include the work's
138 | System Libraries, or general-purpose tools or generally available free
139 | programs which are used unmodified in performing those activities but
140 | which are not part of the work. For example, Corresponding Source
141 | includes interface definition files associated with source files for
142 | the work, and the source code for shared libraries and dynamically
143 | linked subprograms that the work is specifically designed to require,
144 | such as by intimate data communication or control flow between those
145 | subprograms and other parts of the work.
146 |
147 | The Corresponding Source need not include anything that users
148 | can regenerate automatically from other parts of the Corresponding
149 | Source.
150 |
151 | The Corresponding Source for a work in source code form is that
152 | same work.
153 |
154 | 2. Basic Permissions.
155 |
156 | All rights granted under this License are granted for the term of
157 | copyright on the Program, and are irrevocable provided the stated
158 | conditions are met. This License explicitly affirms your unlimited
159 | permission to run the unmodified Program. The output from running a
160 | covered work is covered by this License only if the output, given its
161 | content, constitutes a covered work. This License acknowledges your
162 | rights of fair use or other equivalent, as provided by copyright law.
163 |
164 | You may make, run and propagate covered works that you do not
165 | convey, without conditions so long as your license otherwise remains
166 | in force. You may convey covered works to others for the sole purpose
167 | of having them make modifications exclusively for you, or provide you
168 | with facilities for running those works, provided that you comply with
169 | the terms of this License in conveying all material for which you do
170 | not control copyright. Those thus making or running the covered works
171 | for you must do so exclusively on your behalf, under your direction
172 | and control, on terms that prohibit them from making any copies of
173 | your copyrighted material outside their relationship with you.
174 |
175 | Conveying under any other circumstances is permitted solely under
176 | the conditions stated below. Sublicensing is not allowed; section 10
177 | makes it unnecessary.
178 |
179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 |
181 | No covered work shall be deemed part of an effective technological
182 | measure under any applicable law fulfilling obligations under article
183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 | similar laws prohibiting or restricting circumvention of such
185 | measures.
186 |
187 | When you convey a covered work, you waive any legal power to forbid
188 | circumvention of technological measures to the extent such circumvention
189 | is effected by exercising rights under this License with respect to
190 | the covered work, and you disclaim any intention to limit operation or
191 | modification of the work as a means of enforcing, against the work's
192 | users, your or third parties' legal rights to forbid circumvention of
193 | technological measures.
194 |
195 | 4. Conveying Verbatim Copies.
196 |
197 | You may convey verbatim copies of the Program's source code as you
198 | receive it, in any medium, provided that you conspicuously and
199 | appropriately publish on each copy an appropriate copyright notice;
200 | keep intact all notices stating that this License and any
201 | non-permissive terms added in accord with section 7 apply to the code;
202 | keep intact all notices of the absence of any warranty; and give all
203 | recipients a copy of this License along with the Program.
204 |
205 | You may charge any price or no price for each copy that you convey,
206 | and you may offer support or warranty protection for a fee.
207 |
208 | 5. Conveying Modified Source Versions.
209 |
210 | You may convey a work based on the Program, or the modifications to
211 | produce it from the Program, in the form of source code under the
212 | terms of section 4, provided that you also meet all of these conditions:
213 |
214 | a) The work must carry prominent notices stating that you modified
215 | it, and giving a relevant date.
216 |
217 | b) The work must carry prominent notices stating that it is
218 | released under this License and any conditions added under section
219 | 7. This requirement modifies the requirement in section 4 to
220 | "keep intact all notices".
221 |
222 | c) You must license the entire work, as a whole, under this
223 | License to anyone who comes into possession of a copy. This
224 | License will therefore apply, along with any applicable section 7
225 | additional terms, to the whole of the work, and all its parts,
226 | regardless of how they are packaged. This License gives no
227 | permission to license the work in any other way, but it does not
228 | invalidate such permission if you have separately received it.
229 |
230 | d) If the work has interactive user interfaces, each must display
231 | Appropriate Legal Notices; however, if the Program has interactive
232 | interfaces that do not display Appropriate Legal Notices, your
233 | work need not make them do so.
234 |
235 | A compilation of a covered work with other separate and independent
236 | works, which are not by their nature extensions of the covered work,
237 | and which are not combined with it such as to form a larger program,
238 | in or on a volume of a storage or distribution medium, is called an
239 | "aggregate" if the compilation and its resulting copyright are not
240 | used to limit the access or legal rights of the compilation's users
241 | beyond what the individual works permit. Inclusion of a covered work
242 | in an aggregate does not cause this License to apply to the other
243 | parts of the aggregate.
244 |
245 | 6. Conveying Non-Source Forms.
246 |
247 | You may convey a covered work in object code form under the terms
248 | of sections 4 and 5, provided that you also convey the
249 | machine-readable Corresponding Source under the terms of this License,
250 | in one of these ways:
251 |
252 | a) Convey the object code in, or embodied in, a physical product
253 | (including a physical distribution medium), accompanied by the
254 | Corresponding Source fixed on a durable physical medium
255 | customarily used for software interchange.
256 |
257 | b) Convey the object code in, or embodied in, a physical product
258 | (including a physical distribution medium), accompanied by a
259 | written offer, valid for at least three years and valid for as
260 | long as you offer spare parts or customer support for that product
261 | model, to give anyone who possesses the object code either (1) a
262 | copy of the Corresponding Source for all the software in the
263 | product that is covered by this License, on a durable physical
264 | medium customarily used for software interchange, for a price no
265 | more than your reasonable cost of physically performing this
266 | conveying of source, or (2) access to copy the
267 | Corresponding Source from a network server at no charge.
268 |
269 | c) Convey individual copies of the object code with a copy of the
270 | written offer to provide the Corresponding Source. This
271 | alternative is allowed only occasionally and noncommercially, and
272 | only if you received the object code with such an offer, in accord
273 | with subsection 6b.
274 |
275 | d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 |
288 | e) Convey the object code using peer-to-peer transmission, provided
289 | you inform other peers where the object code and Corresponding
290 | Source of the work are being offered to the general public at no
291 | charge under subsection 6d.
292 |
293 | A separable portion of the object code, whose source code is excluded
294 | from the Corresponding Source as a System Library, need not be
295 | included in conveying the object code work.
296 |
297 | A "User Product" is either (1) a "consumer product", which means any
298 | tangible personal property which is normally used for personal, family,
299 | or household purposes, or (2) anything designed or sold for incorporation
300 | into a dwelling. In determining whether a product is a consumer product,
301 | doubtful cases shall be resolved in favor of coverage. For a particular
302 | product received by a particular user, "normally used" refers to a
303 | typical or common use of that class of product, regardless of the status
304 | of the particular user or of the way in which the particular user
305 | actually uses, or expects or is expected to use, the product. A product
306 | is a consumer product regardless of whether the product has substantial
307 | commercial, industrial or non-consumer uses, unless such uses represent
308 | the only significant mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to install
312 | and execute modified versions of a covered work in that User Product from
313 | a modified version of its Corresponding Source. The information must
314 | suffice to ensure that the continued functioning of the modified object
315 | code is in no case prevented or interfered with solely because
316 | modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or updates
331 | for a work that has been modified or installed by the recipient, or for
332 | the User Product in which it has been modified or installed. Access to a
333 | network may be denied when the modification itself materially and
334 | adversely affects the operation of the network or violates the rules and
335 | protocols for communication across the network.
336 |
337 | Corresponding Source conveyed, and Installation Information provided,
338 | in accord with this section must be in a format that is publicly
339 | documented (and with an implementation available to the public in
340 | source code form), and must require no special password or key for
341 | unpacking, reading or copying.
342 |
343 | 7. Additional Terms.
344 |
345 | "Additional permissions" are terms that supplement the terms of this
346 | License by making exceptions from one or more of its conditions.
347 | Additional permissions that are applicable to the entire Program shall
348 | be treated as though they were included in this License, to the extent
349 | that they are valid under applicable law. If additional permissions
350 | apply only to part of the Program, that part may be used separately
351 | under those permissions, but the entire Program remains governed by
352 | this License without regard to the additional permissions.
353 |
354 | When you convey a copy of a covered work, you may at your option
355 | remove any additional permissions from that copy, or from any part of
356 | it. (Additional permissions may be written to require their own
357 | removal in certain cases when you modify the work.) You may place
358 | additional permissions on material, added by you to a covered work,
359 | for which you have or can give appropriate copyright permission.
360 |
361 | Notwithstanding any other provision of this License, for material you
362 | add to a covered work, you may (if authorized by the copyright holders of
363 | that material) supplement the terms of this License with terms:
364 |
365 | a) Disclaiming warranty or limiting liability differently from the
366 | terms of sections 15 and 16 of this License; or
367 |
368 | b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 |
372 | c) Prohibiting misrepresentation of the origin of that material, or
373 | requiring that modified versions of such material be marked in
374 | reasonable ways as different from the original version; or
375 |
376 | d) Limiting the use for publicity purposes of names of licensors or
377 | authors of the material; or
378 |
379 | e) Declining to grant rights under trademark law for use of some
380 | trade names, trademarks, or service marks; or
381 |
382 | f) Requiring indemnification of licensors and authors of that
383 | material by anyone who conveys the material (or modified versions of
384 | it) with contractual assumptions of liability to the recipient, for
385 | any liability that these contractual assumptions directly impose on
386 | those licensors and authors.
387 |
388 | All other non-permissive additional terms are considered "further
389 | restrictions" within the meaning of section 10. If the Program as you
390 | received it, or any part of it, contains a notice stating that it is
391 | governed by this License along with a term that is a further
392 | restriction, you may remove that term. If a license document contains
393 | a further restriction but permits relicensing or conveying under this
394 | License, you may add to a covered work material governed by the terms
395 | of that license document, provided that the further restriction does
396 | not survive such relicensing or conveying.
397 |
398 | If you add terms to a covered work in accord with this section, you
399 | must place, in the relevant source files, a statement of the
400 | additional terms that apply to those files, or a notice indicating
401 | where to find the applicable terms.
402 |
403 | Additional terms, permissive or non-permissive, may be stated in the
404 | form of a separately written license, or stated as exceptions;
405 | the above requirements apply either way.
406 |
407 | 8. Termination.
408 |
409 | You may not propagate or modify a covered work except as expressly
410 | provided under this License. Any attempt otherwise to propagate or
411 | modify it is void, and will automatically terminate your rights under
412 | this License (including any patent licenses granted under the third
413 | paragraph of section 11).
414 |
415 | However, if you cease all violation of this License, then your
416 | license from a particular copyright holder is reinstated (a)
417 | provisionally, unless and until the copyright holder explicitly and
418 | finally terminates your license, and (b) permanently, if the copyright
419 | holder fails to notify you of the violation by some reasonable means
420 | prior to 60 days after the cessation.
421 |
422 | Moreover, your license from a particular copyright holder is
423 | reinstated permanently if the copyright holder notifies you of the
424 | violation by some reasonable means, this is the first time you have
425 | received notice of violation of this License (for any work) from that
426 | copyright holder, and you cure the violation prior to 30 days after
427 | your receipt of the notice.
428 |
429 | Termination of your rights under this section does not terminate the
430 | licenses of parties who have received copies or rights from you under
431 | this License. If your rights have been terminated and not permanently
432 | reinstated, you do not qualify to receive new licenses for the same
433 | material under section 10.
434 |
435 | 9. Acceptance Not Required for Having Copies.
436 |
437 | You are not required to accept this License in order to receive or
438 | run a copy of the Program. Ancillary propagation of a covered work
439 | occurring solely as a consequence of using peer-to-peer transmission
440 | to receive a copy likewise does not require acceptance. However,
441 | nothing other than this License grants you permission to propagate or
442 | modify any covered work. These actions infringe copyright if you do
443 | not accept this License. Therefore, by modifying or propagating a
444 | covered work, you indicate your acceptance of this License to do so.
445 |
446 | 10. Automatic Licensing of Downstream Recipients.
447 |
448 | Each time you convey a covered work, the recipient automatically
449 | receives a license from the original licensors, to run, modify and
450 | propagate that work, subject to this License. You are not responsible
451 | for enforcing compliance by third parties with this License.
452 |
453 | An "entity transaction" is a transaction transferring control of an
454 | organization, or substantially all assets of one, or subdividing an
455 | organization, or merging organizations. If propagation of a covered
456 | work results from an entity transaction, each party to that
457 | transaction who receives a copy of the work also receives whatever
458 | licenses to the work the party's predecessor in interest had or could
459 | give under the previous paragraph, plus a right to possession of the
460 | Corresponding Source of the work from the predecessor in interest, if
461 | the predecessor has it or can get it with reasonable efforts.
462 |
463 | You may not impose any further restrictions on the exercise of the
464 | rights granted or affirmed under this License. For example, you may
465 | not impose a license fee, royalty, or other charge for exercise of
466 | rights granted under this License, and you may not initiate litigation
467 | (including a cross-claim or counterclaim in a lawsuit) alleging that
468 | any patent claim is infringed by making, using, selling, offering for
469 | sale, or importing the Program or any portion of it.
470 |
471 | 11. Patents.
472 |
473 | A "contributor" is a copyright holder who authorizes use under this
474 | License of the Program or a work on which the Program is based. The
475 | work thus licensed is called the contributor's "contributor version".
476 |
477 | A contributor's "essential patent claims" are all patent claims
478 | owned or controlled by the contributor, whether already acquired or
479 | hereafter acquired, that would be infringed by some manner, permitted
480 | by this License, of making, using, or selling its contributor version,
481 | but do not include claims that would be infringed only as a
482 | consequence of further modification of the contributor version. For
483 | purposes of this definition, "control" includes the right to grant
484 | patent sublicenses in a manner consistent with the requirements of
485 | this License.
486 |
487 | Each contributor grants you a non-exclusive, worldwide, royalty-free
488 | patent license under the contributor's essential patent claims, to
489 | make, use, sell, offer for sale, import and otherwise run, modify and
490 | propagate the contents of its contributor version.
491 |
492 | In the following three paragraphs, a "patent license" is any express
493 | agreement or commitment, however denominated, not to enforce a patent
494 | (such as an express permission to practice a patent or covenant not to
495 | sue for patent infringement). To "grant" such a patent license to a
496 | party means to make such an agreement or commitment not to enforce a
497 | patent against the party.
498 |
499 | If you convey a covered work, knowingly relying on a patent license,
500 | and the Corresponding Source of the work is not available for anyone
501 | to copy, free of charge and under the terms of this License, through a
502 | publicly available network server or other readily accessible means,
503 | then you must either (1) cause the Corresponding Source to be so
504 | available, or (2) arrange to deprive yourself of the benefit of the
505 | patent license for this particular work, or (3) arrange, in a manner
506 | consistent with the requirements of this License, to extend the patent
507 | license to downstream recipients. "Knowingly relying" means you have
508 | actual knowledge that, but for the patent license, your conveying the
509 | covered work in a country, or your recipient's use of the covered work
510 | in a country, would infringe one or more identifiable patents in that
511 | country that you have reason to believe are valid.
512 |
513 | If, pursuant to or in connection with a single transaction or
514 | arrangement, you convey, or propagate by procuring conveyance of, a
515 | covered work, and grant a patent license to some of the parties
516 | receiving the covered work authorizing them to use, propagate, modify
517 | or convey a specific copy of the covered work, then the patent license
518 | you grant is automatically extended to all recipients of the covered
519 | work and works based on it.
520 |
521 | A patent license is "discriminatory" if it does not include within
522 | the scope of its coverage, prohibits the exercise of, or is
523 | conditioned on the non-exercise of one or more of the rights that are
524 | specifically granted under this License. You may not convey a covered
525 | work if you are a party to an arrangement with a third party that is
526 | in the business of distributing software, under which you make payment
527 | to the third party based on the extent of your activity of conveying
528 | the work, and under which the third party grants, to any of the
529 | parties who would receive the covered work from you, a discriminatory
530 | patent license (a) in connection with copies of the covered work
531 | conveyed by you (or copies made from those copies), or (b) primarily
532 | for and in connection with specific products or compilations that
533 | contain the covered work, unless you entered into that arrangement,
534 | or that patent license was granted, prior to 28 March 2007.
535 |
536 | Nothing in this License shall be construed as excluding or limiting
537 | any implied license or other defenses to infringement that may
538 | otherwise be available to you under applicable patent law.
539 |
540 | 12. No Surrender of Others' Freedom.
541 |
542 | If conditions are imposed on you (whether by court order, agreement or
543 | otherwise) that contradict the conditions of this License, they do not
544 | excuse you from the conditions of this License. If you cannot convey a
545 | covered work so as to satisfy simultaneously your obligations under this
546 | License and any other pertinent obligations, then as a consequence you may
547 | not convey it at all. For example, if you agree to terms that obligate you
548 | to collect a royalty for further conveying from those to whom you convey
549 | the Program, the only way you could satisfy both those terms and this
550 | License would be to refrain entirely from conveying the Program.
551 |
552 | 13. Use with the GNU Affero General Public License.
553 |
554 | Notwithstanding any other provision of this License, you have
555 | permission to link or combine any covered work with a work licensed
556 | under version 3 of the GNU Affero General Public License into a single
557 | combined work, and to convey the resulting work. The terms of this
558 | License will continue to apply to the part which is the covered work,
559 | but the special requirements of the GNU Affero General Public License,
560 | section 13, concerning interaction through a network will apply to the
561 | combination as such.
562 |
563 | 14. Revised Versions of this License.
564 |
565 | The Free Software Foundation may publish revised and/or new versions of
566 | the GNU General Public License from time to time. Such new versions will
567 | be similar in spirit to the present version, but may differ in detail to
568 | address new problems or concerns.
569 |
570 | Each version is given a distinguishing version number. If the
571 | Program specifies that a certain numbered version of the GNU General
572 | Public License "or any later version" applies to it, you have the
573 | option of following the terms and conditions either of that numbered
574 | version or of any later version published by the Free Software
575 | Foundation. If the Program does not specify a version number of the
576 | GNU General Public License, you may choose any version ever published
577 | by the Free Software Foundation.
578 |
579 | If the Program specifies that a proxy can decide which future
580 | versions of the GNU General Public License can be used, that proxy's
581 | public statement of acceptance of a version permanently authorizes you
582 | to choose that version for the Program.
583 |
584 | Later license versions may give you additional or different
585 | permissions. However, no additional obligations are imposed on any
586 | author or copyright holder as a result of your choosing to follow a
587 | later version.
588 |
589 | 15. Disclaimer of Warranty.
590 |
591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 |
600 | 16. Limitation of Liability.
601 |
602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 | SUCH DAMAGES.
611 |
612 | 17. Interpretation of Sections 15 and 16.
613 |
614 | If the disclaimer of warranty and limitation of liability provided
615 | above cannot be given local legal effect according to their terms,
616 | reviewing courts shall apply local law that most closely approximates
617 | an absolute waiver of all civil liability in connection with the
618 | Program, unless a warranty or assumption of liability accompanies a
619 | copy of the Program in return for a fee.
620 |
621 | END OF TERMS AND CONDITIONS
622 |
623 | How to Apply These Terms to Your New Programs
624 |
625 | If you develop a new program, and you want it to be of the greatest
626 | possible use to the public, the best way to achieve this is to make it
627 | free software which everyone can redistribute and change under these terms.
628 |
629 | To do so, attach the following notices to the program. It is safest
630 | to attach them to the start of each source file to most effectively
631 | state the exclusion of warranty; and each file should have at least
632 | the "copyright" line and a pointer to where the full notice is found.
633 |
634 |
635 | Copyright (C)
636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU General Public License for more details.
646 |
647 | You should have received a copy of the GNU General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | Copyright (C)
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
675 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: all test-upload upload test install-poetry install format
2 |
3 | test-upload:
4 | poetry publish --build -r testpypi
5 |
6 | upload:
7 | poetry publish --build
8 |
9 | test:
10 | poetry run nosetests
11 |
12 | install-poetry:
13 | curl -sSL https://install.python-poetry.org | python -
14 |
15 | install: install-poetry
16 | git config --local core.hooksPath .githooks
17 | pip install -r requirements.txt
18 |
19 | format:
20 | poetry run isort . && poetry run black . && poetry run pflake8 .
21 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | python-shogi: a pure Python shogi library
2 | =========================================
3 |
4 | .. image:: https://coveralls.io/repos/gunyarakun/python-shogi/badge.svg
5 | :target: https://coveralls.io/r/gunyarakun/python-shogi
6 |
7 | .. image:: https://badge.fury.io/py/python-shogi.svg
8 | :target: https://pypi.python.org/pypi/python-shogi
9 |
10 | .. image:: https://github.com/gunyarakun/python-shogi/actions/workflows/pythonpackage.yml/badge.svg
11 | :target: https://github.com/gunyarakun/python-shogi/actions/workflows/pythonpackage.yml
12 |
13 | .. image:: https://github.com/gunyarakun/python-shogi/actions/workflows/codeql-analysis.yml/badge.svg
14 | :target: https://github.com/gunyarakun/python-shogi/actions/workflows/codeql-analysis.yml
15 |
16 | Introduction
17 | ------------
18 |
19 | This is the module for shogi written in Pure Python. It's based on python-chess `commit `__
20 |
21 |
22 | This is the scholars mate in python-shogi:
23 |
24 | .. code:: python
25 |
26 | >>> import shogi
27 |
28 | >>> board = shogi.Board()
29 |
30 | >>> board.push(shogi.Move.from_usi('7g7f'))
31 |
32 | >>> board.push_usi('3c3d')
33 | Move.from_usi('3c3d')
34 | >>> board.push_usi('8h2b+')
35 | Move.from_usi('8h2b+')
36 | >>> board.push_usi('4a5b')
37 | Move.from_usi('4a5b')
38 | >>> board.push_usi('B*4b')
39 | Move.from_usi('B*4b')
40 | >>> board.push_usi('5a4a')
41 | Move.from_usi('5a4a')
42 | >>> board.push_usi('2b3a')
43 | Move.from_usi('2b3a')
44 | >>> board.is_checkmate()
45 | True
46 |
47 | Features
48 | --------
49 |
50 | * Supports Python 3.3+.
51 |
52 | * Supports standard shogi (hon shogi)
53 |
54 | * Legal move generator and move validation.
55 |
56 | .. code:: python
57 |
58 | >>> shogi.Move.from_usi("5i5a") in board.legal_moves
59 | False
60 |
61 | * Make and unmake moves.
62 |
63 | .. code:: python
64 |
65 | >>> last_move = board.pop() # Unmake last move
66 | >>> last_move
67 | Move.from_usi('2b3a')
68 |
69 | >>> board.push(last_move) # Restore
70 |
71 | * Show a simple ASCII board.
72 |
73 | .. code:: python
74 |
75 | >>> print(board)
76 | l n s g . k +B n l
77 | . r . . g B . . .
78 | p p p p p p . p p
79 | . . . . . . p . .
80 | . . . . . . . . .
81 | . . P . . . . . .
82 | P P . P P P P P P
83 | . . . . . . . R .
84 | L N S G K G S N L
85 |
86 | S*1
87 |
88 | * Show a KIF style board.
89 |
90 | .. code:: python
91 |
92 | >>> print(board.kif_str())
93 | 後手の持駒:
94 | 9 8 7 6 5 4 3 2 1
95 | +---------------------------+
96 | |v香v桂v銀v金 ・v玉 馬v桂v香|一
97 | | ・v飛 ・ ・v金 角 ・ ・ ・|二
98 | |v歩v歩v歩v歩v歩v歩 ・v歩v歩|三
99 | | ・ ・ ・ ・ ・ ・v歩 ・ ・|四
100 | | ・ ・ ・ ・ ・ ・ ・ ・ ・|五
101 | | ・ ・ 歩 ・ ・ ・ ・ ・ ・|六
102 | | 歩 歩 ・ 歩 歩 歩 歩 歩 歩|七
103 | | ・ ・ ・ ・ ・ ・ ・ 飛 ・|八
104 | | 香 桂 銀 金 玉 金 銀 桂 香|九
105 | +---------------------------+
106 | 先手の持駒: 銀
107 |
108 | * Detects checkmates, stalemates.
109 |
110 | .. code:: python
111 |
112 | >>> board.is_stalemate()
113 | False
114 | >>> board.is_game_over()
115 | True
116 |
117 | * Detects repetitions. Has a half move clock.
118 |
119 | .. code:: python
120 |
121 | >>> board.is_fourfold_repetition()
122 | False
123 | >>> board.move_number
124 | 8
125 |
126 | * Detects checks and attacks.
127 |
128 | .. code:: python
129 |
130 | >>> board.is_check()
131 | True
132 | >>> board.is_attacked_by(shogi.BLACK, shogi.A4)
133 | True
134 | >>> attackers = board.attackers(shogi.BLACK, shogi.H5)
135 | >>> attackers
136 | SquareSet(0b111000010000000000000000000000000000000000000000000000000000000000000000000000)
137 | >>> shogi.H2 in attackers
138 | True
139 | >>> print(attackers)
140 | . . . . . . . . .
141 | . . . . . . . . .
142 | . . . . . . . . .
143 | . . . . . . . . .
144 | . . . . . . . . .
145 | . . . . . . . . .
146 | . . . . . . . . .
147 | . . . . . . . 1 .
148 | . . . 1 1 1 . . .
149 |
150 | * Parses and creates USI representation of moves.
151 |
152 | .. code:: python
153 |
154 | >>> board = shogi.Board()
155 | >>> shogi.Move(shogi.E2, shogi.E4).usi()
156 | '2e4e'
157 |
158 | * Parses and creates SFENs
159 |
160 | .. code:: python
161 |
162 | >>> board.sfen()
163 | 'lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL b - 1'
164 | >>> board.piece_at(shogi.I5)
165 | Piece.from_symbol('K')
166 |
167 | * Read KIFs.
168 |
169 | .. code:: python
170 |
171 | >>> import shogi.KIF
172 |
173 | >>> kif = shogi.KIF.Parser.parse_file('data/games/habu-fujii-2006.kif')[0]
174 |
175 | >>> kif['names'][shogi.BLACK]
176 | '羽生善治'
177 | >>> kif['names'][shogi.WHITE]
178 | '藤井猛'
179 | >>> kif['moves'] # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
180 | ['7g7f',
181 | '3c3d',
182 | ...,
183 | '9a9b',
184 | '7a7b+']
185 | >>> kif['win']
186 | 'b'
187 |
188 | * Export to KIFs.
189 |
190 | .. code:: python
191 |
192 | >>> import shogi
193 | >>> import shogi.KIF
194 |
195 | >>> board = shogi.Board()
196 | >>> shogi.KIF.Exporter.kif_move_from('7g7f', board)
197 | '7六歩(77)'
198 |
199 | >>> sfen_summary = {'moves': ['7g7f', '3c3d'], 'sfen': 'lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL b - 1', 'names': ['羽生善治', '藤井猛'], 'win': 'w'}
200 | >>> shogi.KIF.Exporter.kif(sfen_summary)
201 | 開始日時: \r
202 | 終了日時: \r
203 | 手合割:平手\r
204 | 先手:羽生善治\r
205 | 後手:藤井猛\r
206 | 手数----指手---------消費時間-- \r
207 | 1 7六歩(77) \r
208 | 2 3四歩(33) \r
209 | 3 投了 \r
210 | まで2手で後手の勝ち\r
211 |
212 | * Communicate with a CSA protocol.
213 |
214 | Please see `random_csa_tcp_match `_.
215 |
216 | * Parse professional shogi players' name
217 |
218 | >>> import shogi.Person
219 |
220 | >>> shogi.Person.Name.is_professional('羽生 善治 名人・棋聖・王位・王座')
221 | True
222 |
223 | Performance
224 | -----------
225 | python-shogi is not intended to be used by serious shogi engines where
226 | performance is critical. The goal is rather to create a simple and relatively
227 | highlevel library.
228 |
229 | You can install the `gmpy2 `__ or `gmpy `__ modules
230 | in order to get a slight performance boost on basic operations like bit scans
231 | and population counts.
232 |
233 | python-shogi will only ever import very basic general (non-shogi-related)
234 | operations from native libraries. All logic is pure Python. There will always
235 | be pure Python fallbacks.
236 |
237 | Installing
238 | ----------
239 |
240 | * With pip:
241 |
242 | ::
243 |
244 | pip install python-shogi
245 |
246 | How to test
247 | -----------
248 |
249 | ::
250 |
251 | > make test
252 |
253 | If you want to print lines from the standard output, execute nosetests like following.
254 |
255 | ::
256 |
257 | > poetry run nosetests -s
258 |
259 | How to release
260 | --------------
261 |
262 | ::
263 |
264 | poetry config repositories.testpypi https://test.pypi.org/legacy/
265 | # poetry config pypi-token.testpypi "Test PyPI API Token"
266 | make test-upload
267 | # poetry config pypi-token.pypi "PyPI API Token"
268 | make upload
269 |
270 | ToDo
271 | ----
272 |
273 | - Support board.generate_attacks() and use it in board.is_attacked_by() and board.attacker_mask().
274 |
275 | - Remove rotated bitboards and support `Shatranj-style direct lookup
276 | `_ like recent python-chess.
277 |
278 | - Support %MATTA etc. in CSA TCP Protocol.
279 |
280 | - Support board.is_pinned() and board.pin().
281 |
--------------------------------------------------------------------------------
/data/games/habu-fujii-2006.kif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gunyarakun/python-shogi/a814cbff51b0703df2503ee59408fefd2e464ae5/data/games/habu-fujii-2006.kif
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.poetry]
2 | name = "python-shogi"
3 | version = "1.1.1"
4 | description = "A pure Python shogi library with move generation and validation and handling of common formats."
5 | authors = ["Tasuku SUENAGA a.k.a. gunyarakun "]
6 | keywords = ["shogi", "csa", "kif"]
7 | readme = "README.rst"
8 | homepage = "https://github.com/gunyarakun/python-shogi"
9 | packages = [{ include = "shogi"}]
10 | license = "GPL-3.0-only"
11 | classifiers = [
12 | 'Development Status :: 5 - Production/Stable',
13 | 'Intended Audience :: Developers',
14 | 'Operating System :: OS Independent',
15 | 'Topic :: Games/Entertainment :: Board Games',
16 | 'Topic :: Software Development :: Libraries :: Python Modules',
17 | ]
18 |
19 | [tool.poetry.dependencies]
20 | python = ">=3.4"
21 |
22 | [tool.black]
23 | target-version = ["py311"]
24 | line-length = 120
25 |
26 | [tool.isort]
27 | line_length = 120
28 | multi_line_output = 3
29 | include_trailing_comma = true
30 | known_local_folder=["config",]
31 |
32 | [tool.flake8]
33 | max-line-length = 120
34 | max-complexity = 18
35 | ignore = "E203,E266,E501,W503,"
36 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | # I would like to put all the dependencies into pyproject.toml, but the Python version conflicts.
2 |
3 | # For tests
4 | mock==5.0.1
5 | nose-py3==1.6.0
6 |
7 | # For format
8 | pyproject-flake8==6.0.0
9 | black==23.1.0
10 | isort==5.12.0
11 |
--------------------------------------------------------------------------------
/scripts/random_csa_tcp_match:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | import random
5 | import sys
6 |
7 | import shogi
8 | from shogi import CSA
9 |
10 | if not hasattr(sys.stdout, 'buffer'):
11 | import codecs
12 | import locale
13 | sys.stdout = codecs.getwriter(locale.getpreferredencoding())(sys.stdout)
14 |
15 | if len(sys.argv) != 4:
16 | print('Usage: {0} host_name user_name password'.format(sys.argv[0]))
17 | sys.exit(1)
18 |
19 | while True:
20 | ct = CSA.TCPProtocol(sys.argv[1], 4081)
21 | # ct = CSAProtocol('wdoor.c.u-tokyo.ac.jp', 4081)
22 | ct.login(sys.argv[2], sys.argv[3])
23 | game_summary = ct.wait_match()
24 | sfen = game_summary['summary']['sfen']
25 | my_color = game_summary['my_color']
26 | board = shogi.Board(sfen)
27 | ct.agree()
28 |
29 | while True:
30 | print(board.sfen())
31 | print(board.kif_str())
32 |
33 | if board.turn == my_color:
34 | moves = []
35 | for move in board.legal_moves:
36 | moves.append(move)
37 |
38 | if len(moves) == 0:
39 | ct.resign()
40 | break
41 | else:
42 | next_move = random.choice(moves)
43 | print('ADD SELF MOVE: {0}'.format(next_move))
44 | board.push(next_move)
45 | ct.move(board.pieces[next_move.to_square], my_color, next_move)
46 | else:
47 | (turn, usi, spend_time, message) = ct.wait_server_message(board)
48 |
49 | if message is not None:
50 | print('MESSAGE: {0}'.format(CSA.SERVER_MESSAGE_SYMBOLS[message]))
51 | if message == CSA.WIN:
52 | break
53 | elif message == CSA.LOSE:
54 | break
55 | elif message == CSA.CENSORED:
56 | break
57 | elif message == CSA.CHUDAN:
58 | break
59 | else:
60 | if turn != board.turn:
61 | raise ValueError('Invalid turn')
62 | move = shogi.Move.from_usi(usi)
63 | print('ADD OPPONENT MOVE: {0}'.format(move))
64 | board.push(move)
65 |
--------------------------------------------------------------------------------
/scripts/random_self_match:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | import random
5 | import sys
6 |
7 | import shogi
8 |
9 | if not hasattr(sys.stdout, 'buffer'):
10 | import codecs
11 | import locale
12 | sys.stdout = codecs.getwriter(locale.getpreferredencoding())(sys.stdout)
13 |
14 | board = shogi.Board()
15 |
16 | while True:
17 | print(board.sfen())
18 | print(board.kif_str())
19 |
20 | moves = []
21 | for move in board.legal_moves:
22 | moves.append(move)
23 |
24 | if len(moves) == 0:
25 | break
26 |
27 | next_move = random.choice(moves)
28 |
29 | print(next_move)
30 |
31 | board.push(next_move)
32 |
--------------------------------------------------------------------------------
/scripts/random_shogidokoro_match:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | import random
5 | import sys
6 | import time
7 |
8 | import shogi
9 | from shogi import CSA
10 |
11 | if len(sys.argv) != 4:
12 | print('Usage: {0} host_name user_name password'.format(sys.argv[0]))
13 | sys.exit(1)
14 |
15 | ct = CSA.TCPProtocol(sys.argv[1], 4081)
16 | ct.login(sys.argv[2], sys.argv[3])
17 | game_summary = ct.wait_match()
18 | sfen = game_summary['summary']['sfen']
19 | my_color = game_summary['my_color']
20 | board = shogi.Board(sfen)
21 | ct.agree()
22 |
23 | while True:
24 | print("pc turn")
25 | moves = []
26 | for move in board.legal_moves:
27 | moves.append(move)
28 |
29 | if len(moves) == 0:
30 | ct.resign()
31 | break
32 |
33 | next_move = random.choice(moves)
34 | board.push(next_move)
35 | print(board.kif_str())
36 | print('pc turn end. and waiting Player')
37 | recieved_cmd = ct.move(
38 | board.pieces[next_move.to_square], my_color, next_move)
39 |
40 | print('Player moved')
41 | (_, usi, spend_time, message) = ct.parse_server_message(
42 | recieved_cmd, board)
43 | move = shogi.Move.from_usi(usi)
44 | board.push(move)
45 | print(board.kif_str())
46 | print('Player moving handle end. and now pc turn')
47 |
--------------------------------------------------------------------------------
/shogi/CSA.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # This file is part of the python-shogi library.
4 | # Copyright (C) 2015- Tasuku SUENAGA
5 | #
6 | # This program is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, either version 3 of the License, or
9 | # (at your option) any later version.
10 | #
11 | # This program is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | # GNU General Public License for more details.
15 | #
16 | # You should have received a copy of the GNU General Public License
17 | # along with this program. If not, see .
18 |
19 | import collections
20 | import re
21 | import socket
22 | import threading
23 | import time
24 |
25 | import shogi
26 |
27 | DEFAULT_PORT = 4081
28 | PING_SLEEP_DURATION = 1
29 | PING_DURATION = 60
30 | SOCKET_RECV_SIZE = 4096
31 | BLOCK_RECV_SLEEP_DURATION = 0.1
32 |
33 | COLOR_SYMBOLS = ["+", "-"]
34 | PIECE_SYMBOLS = ["* ", "FU", "KY", "KE", "GI", "KI", "KA", "HI", "OU", "TO", "NY", "NK", "NG", "UM", "RY"]
35 | SQUARE_NAMES = [
36 | "91",
37 | "81",
38 | "71",
39 | "61",
40 | "51",
41 | "41",
42 | "31",
43 | "21",
44 | "11",
45 | "92",
46 | "82",
47 | "72",
48 | "62",
49 | "52",
50 | "42",
51 | "32",
52 | "22",
53 | "12",
54 | "93",
55 | "83",
56 | "73",
57 | "63",
58 | "53",
59 | "43",
60 | "33",
61 | "23",
62 | "13",
63 | "94",
64 | "84",
65 | "74",
66 | "64",
67 | "54",
68 | "44",
69 | "34",
70 | "24",
71 | "14",
72 | "95",
73 | "85",
74 | "75",
75 | "65",
76 | "55",
77 | "45",
78 | "35",
79 | "25",
80 | "15",
81 | "96",
82 | "86",
83 | "76",
84 | "66",
85 | "56",
86 | "46",
87 | "36",
88 | "26",
89 | "16",
90 | "97",
91 | "87",
92 | "77",
93 | "67",
94 | "57",
95 | "47",
96 | "37",
97 | "27",
98 | "17",
99 | "98",
100 | "88",
101 | "78",
102 | "68",
103 | "58",
104 | "48",
105 | "38",
106 | "28",
107 | "18",
108 | "99",
109 | "89",
110 | "79",
111 | "69",
112 | "59",
113 | "49",
114 | "39",
115 | "29",
116 | "19",
117 | ]
118 | SERVER_MESSAGE_SYMBOLS = [
119 | # '#' prefixed
120 | "WIN",
121 | "LOSE",
122 | "DRAW",
123 | "SENNICHITE",
124 | "OUTE_SENNICHITE",
125 | "ILLEGAL_MOVE",
126 | "TIME_UP",
127 | "RESIGN",
128 | "JISHOGI",
129 | "CHUDAN",
130 | "MAX_MOVES",
131 | "CENSORED",
132 | # '%' prefixed
133 | "TORYO",
134 | "KACHI",
135 | ]
136 | SERVER_MESSAGES = [
137 | WIN,
138 | LOSE,
139 | DRAW,
140 | SENNICHITE,
141 | OUTE_SENNICHITE,
142 | ILLEGAL_MOVE,
143 | TIME_UP,
144 | REGISN,
145 | JISHOGI,
146 | CHUDAN,
147 | MAX_MOVES,
148 | CENSORED,
149 | TORYO,
150 | KACHI,
151 | ] = range(0, len(SERVER_MESSAGE_SYMBOLS))
152 |
153 |
154 | class Parser:
155 | @staticmethod
156 | def parse_file(path):
157 | with open(path) as f:
158 | return Parser.parse_str(f.read())
159 |
160 | @staticmethod
161 | def parse_str(csa_str): # noqa: C901
162 | line_no = 1
163 |
164 | sfen = None
165 | board = None
166 | position_lines = []
167 | names = [None, None]
168 | current_turn_str = None
169 | moves = []
170 | lose_color = None
171 | for line in csa_str.split("\n"):
172 | if line == "":
173 | pass
174 | elif line[0] == "'":
175 | pass
176 | elif line[0] == "V":
177 | # Currently just ignoring version
178 | pass
179 | elif line[0] == "N" and line[1] in COLOR_SYMBOLS:
180 | names[COLOR_SYMBOLS.index(line[1])] = line[2:]
181 | elif line[0] == "$":
182 | # Currently just ignoring information
183 | pass
184 | elif line[0] == "P":
185 | position_lines.append(line)
186 | elif line[0] in COLOR_SYMBOLS:
187 | if len(line) == 1:
188 | current_turn_str = line[0]
189 | else:
190 | if not board:
191 | raise ValueError("Board infomation is not defined before a move")
192 | (color, move) = Parser.parse_move_str(line, board)
193 | moves.append(move)
194 | board.push(shogi.Move.from_usi(move))
195 | elif line[0] == "T":
196 | # Currently just ignoring consumed time
197 | pass
198 | elif line[0] == "%":
199 | # End of the game
200 | if not board:
201 | raise ValueError("Board infomation is not defined before a special move")
202 | if line in ["%TORYO", "%TIME_UP", "%ILLEGAL_MOVE"]:
203 | lose_color = board.turn
204 | elif line == "%+ILLEGAL_ACTION":
205 | lose_color = shogi.BLACK
206 | elif line == "%-ILLEGAL_ACTION":
207 | lose_color = shogi.WHITE
208 |
209 | # TODO: Support %MATTA etc.
210 | break
211 | elif line == "/":
212 | raise ValueError("Dont support multiple matches in str")
213 | else:
214 | raise ValueError("Invalid line {0}: {1}".format(line_no, line))
215 | if board is None and current_turn_str:
216 | position = Parser.parse_position(position_lines)
217 | sfen = Exporter.sfen(position["pieces"], position["pieces_in_hand"], current_turn_str, 1)
218 | board = shogi.Board(sfen)
219 | line_no += 1
220 |
221 | if lose_color == shogi.BLACK:
222 | win = "w"
223 | elif lose_color == shogi.WHITE:
224 | win = "b"
225 | else:
226 | win = "-"
227 |
228 | summary = {"names": names, "sfen": sfen, "moves": moves, "win": win}
229 | # NOTE: for future support of multiple matches
230 | return [summary]
231 |
232 | @staticmethod
233 | def parse_move_str(move_str, board):
234 | color = COLOR_SYMBOLS.index(move_str[0])
235 | from_str = move_str[1:3]
236 | to_str = move_str[3:5]
237 | piece_str = move_str[5:7]
238 |
239 | if from_str == "00":
240 | from_square = None
241 | else:
242 | from_square = SQUARE_NAMES.index(from_str)
243 |
244 | to_square = SQUARE_NAMES.index(to_str)
245 | piece_type = PIECE_SYMBOLS.index(piece_str)
246 |
247 | if from_square is None:
248 | return (color, "{0}*{1}".format(shogi.PIECE_SYMBOLS[piece_type].upper(), shogi.SQUARE_NAMES[to_square]))
249 | else:
250 | from_piece_type = board.pieces[from_square]
251 | promotion = from_piece_type != piece_type
252 | return (color, shogi.SQUARE_NAMES[from_square] + shogi.SQUARE_NAMES[to_square] + ("+" if promotion else ""))
253 |
254 | @staticmethod
255 | def parse_position(position_block_lines):
256 | # ex.) P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
257 | # ex.) PI82HI22KA
258 | position = {
259 | "pieces": [None for x in range(81)],
260 | "pieces_in_hand": [
261 | collections.Counter(),
262 | collections.Counter(),
263 | ],
264 | }
265 | for line in position_block_lines:
266 | if line[0] != "P":
267 | if line[0] in COLOR_SYMBOLS:
268 | color = COLOR_SYMBOLS.index(line[0])
269 | if len(line) == 1:
270 | # duplicated data
271 | position["current_turn"] = color
272 | else:
273 | # move
274 | raise NotImplementedError("TODO: parse moves")
275 | else:
276 | raise ValueError("Invalid position line: {0}".format(line))
277 | elif line[1] in COLOR_SYMBOLS:
278 | index = 2
279 | color = COLOR_SYMBOLS.index(line[1])
280 | while index < len(line):
281 | file_index = int(line[index : index + 1])
282 | index += 1
283 | rank_index = int(line[index : index + 1])
284 | index += 1
285 | piece_type = PIECE_SYMBOLS.index(line[index : index + 2])
286 | index += 2
287 | if rank_index == 0 and file_index == 0:
288 | # piece in hand
289 | position["pieces_in_hand"][color][piece_type] += 1
290 | else:
291 | position["pieces"][(rank_index - 1) * 9 + (9 - file_index)] = (piece_type, color)
292 | elif line[1] in ["1", "2", "3", "4", "5", "6", "7", "8", "9"]:
293 | rank_index = int(line[1:2]) - 1
294 | file_index = 0
295 | for index in range(2, 29, 3):
296 | piece_str = line[index : index + 3]
297 | piece_type = PIECE_SYMBOLS.index(piece_str[1:3])
298 | if piece_type:
299 | color = COLOR_SYMBOLS.index(piece_str[0])
300 | piece = (piece_type, color)
301 | else:
302 | piece = None
303 |
304 | position["pieces"][rank_index * 9 + file_index] = piece
305 |
306 | file_index += 1
307 | elif line[1] == "I": # PI
308 | position["pieces"] = [
309 | (2, 1),
310 | (3, 1),
311 | (4, 1),
312 | (5, 1),
313 | (8, 1),
314 | (5, 1),
315 | (4, 1),
316 | (3, 1),
317 | (2, 1),
318 | None,
319 | (7, 1),
320 | None,
321 | None,
322 | None,
323 | None,
324 | None,
325 | (6, 1),
326 | None,
327 | (1, 1),
328 | (1, 1),
329 | (1, 1),
330 | (1, 1),
331 | (1, 1),
332 | (1, 1),
333 | (1, 1),
334 | (1, 1),
335 | (1, 1),
336 | None,
337 | None,
338 | None,
339 | None,
340 | None,
341 | None,
342 | None,
343 | None,
344 | None,
345 | None,
346 | None,
347 | None,
348 | None,
349 | None,
350 | None,
351 | None,
352 | None,
353 | None,
354 | None,
355 | None,
356 | None,
357 | None,
358 | None,
359 | None,
360 | None,
361 | None,
362 | None,
363 | (1, 0),
364 | (1, 0),
365 | (1, 0),
366 | (1, 0),
367 | (1, 0),
368 | (1, 0),
369 | (1, 0),
370 | (1, 0),
371 | (1, 0),
372 | None,
373 | (6, 0),
374 | None,
375 | None,
376 | None,
377 | None,
378 | None,
379 | (7, 0),
380 | None,
381 | (2, 0),
382 | (3, 0),
383 | (4, 0),
384 | (5, 0),
385 | (8, 0),
386 | (5, 0),
387 | (4, 0),
388 | (3, 0),
389 | (2, 0),
390 | ]
391 | index = 2
392 | while index < len(line):
393 | file_index = int(line[index : index + 1])
394 | index += 1
395 | rank_index = int(line[index : index + 1])
396 | index += 1
397 | piece_type = PIECE_SYMBOLS.index(line[index : index + 2])
398 | index += 2
399 | if rank_index == 0 and file_index == 0:
400 | # piece in hand
401 | raise NotImplementedError("TODO: Not implemented komaochi in komadai")
402 | piece_index = (rank_index - 1) * 9 + (9 - file_index)
403 | if position["pieces"][piece_index] is None or position["pieces"][piece_index][0] != piece_type:
404 | raise ValueError("Invalid piece removing on intializing a board")
405 | position["pieces"][piece_index] = None
406 | else:
407 | raise ValueError("Invalid rank/piece in hand: {0}".format(line))
408 | position["pieces_in_hand"] = [
409 | dict(position["pieces_in_hand"][0]),
410 | dict(position["pieces_in_hand"][1]),
411 | ]
412 | return position
413 |
414 |
415 | class Exporter:
416 | @staticmethod
417 | def sfen(pieces, pieces_in_hand, current_turn_char, move_count):
418 | sfen = []
419 | empty = 0
420 |
421 | # Position part.
422 | for square in shogi.SQUARES:
423 | piece_tuple = pieces[square]
424 | if piece_tuple is None:
425 | empty += 1
426 | else:
427 | (piece_type, color) = piece_tuple
428 | piece = shogi.Piece(piece_type, color)
429 |
430 | if empty:
431 | sfen.append(str(empty))
432 | empty = 0
433 | sfen.append(piece.symbol())
434 |
435 | if shogi.BB_SQUARES[square] & shogi.BB_FILE_1:
436 | if empty:
437 | sfen.append(str(empty))
438 | empty = 0
439 |
440 | if square != shogi.I1:
441 | sfen.append("/")
442 |
443 | sfen.append(" ")
444 |
445 | # Side to move.
446 | current_turn = COLOR_SYMBOLS.index(current_turn_char)
447 | if current_turn == shogi.WHITE:
448 | sfen.append("w")
449 | else:
450 | sfen.append("b")
451 |
452 | sfen.append(" ")
453 |
454 | # Pieces in hand
455 | pih_len = 0
456 | for color in shogi.COLORS:
457 | p = pieces_in_hand[color]
458 | pih_len += len(p)
459 | for piece_type in p.keys():
460 | if p[piece_type] > 1:
461 | sfen.append(str(p[piece_type]))
462 | elif p[piece_type] >= 1:
463 | piece = shogi.Piece(piece_type, color)
464 | sfen.append(piece.symbol())
465 | if pih_len == 0:
466 | sfen.append("-")
467 |
468 | sfen.append(" ")
469 |
470 | # Move count
471 | sfen.append(str(move_count))
472 |
473 | sfen_str = "".join(sfen)
474 |
475 | return sfen_str
476 |
477 |
478 | class TCPProtocol:
479 | def __init__(self, host=None, port=0):
480 | if host:
481 | self.open(host, port)
482 |
483 | def open(self, host, port=0):
484 | if not port:
485 | port = DEFAULT_PORT
486 | self.host = host
487 | self.port = port
488 |
489 | self.recv_buf = ""
490 |
491 | self.connect(host, port)
492 |
493 | # Heartbeats
494 | self.heartbeat_thread = CSAHeartbeat(self, PING_SLEEP_DURATION, PING_DURATION)
495 | self.heartbeat_thread.start()
496 |
497 | def connect(self, host, port):
498 | for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM):
499 | af, socktype, proto, canonname, sa = res
500 | try:
501 | self.socket = socket.socket(af, socktype, proto)
502 | self.socket.connect(sa)
503 | except socket.error as msg:
504 | self.msg = msg
505 | if self.socket:
506 | self.socket.close()
507 | self.socket = None
508 | continue
509 | break
510 | if not self.socket:
511 | raise socket.error(self.msg)
512 |
513 | def command(self, command):
514 | self.write(command + "\n")
515 | line = self.read_line()
516 | return line
517 |
518 | def write(self, buf):
519 | self.socket.sendall(buf.encode("utf-8"))
520 |
521 | def read(self):
522 | buf = self.socket.recv(SOCKET_RECV_SIZE).decode("utf-8")
523 | self.recv_buf += buf
524 | return len(buf)
525 |
526 | def read_line(self, block=True):
527 | line = self.read_until("\n", block)
528 | return line
529 |
530 | def read_until(self, target, block=True):
531 | while 1:
532 | if target in self.recv_buf:
533 | (result, self.recv_buf) = self.recv_buf.split(target, 1)
534 | return result
535 | else:
536 | if self.read() == 0:
537 | if block:
538 | time.sleep(BLOCK_RECV_SLEEP_DURATION)
539 | else:
540 | return None
541 |
542 | def ping(self):
543 | line = self.command("")
544 | if line != "":
545 | raise ValueError("Ping return must be empty")
546 |
547 | login_username_re = re.compile(r"\A[-_0-9A-Za-z]+\Z")
548 | login_response_re = re.compile(r"\ALOGIN:([-_0-9A-Za-z]+)( OK)?\Z")
549 |
550 | def login(self, username, password):
551 | if not self.login_username_re.match(username):
552 | raise ValueError("Invalid username.")
553 | if " " in password:
554 | raise ValueError("Invalid password.")
555 |
556 | line = self.command("LOGIN {0} {1}".format(username, password))
557 | line_match = self.login_response_re.match(line)
558 | if line_match:
559 | if line_match.group(2) == " OK":
560 | if line_match.group(1) == username:
561 | return True
562 | elif line_match.group(1) == "incorrect":
563 | raise ValueError("Login failed. Check username and password.")
564 | raise ValueError("Login response was invalid.")
565 |
566 | def login_ex(self, username, password):
567 | if not self.login_username_re.match(username):
568 | raise ValueError("Invalid username.")
569 | if " " in password:
570 | raise ValueError("Invalid password.")
571 |
572 | line = self.command("LOGIN {0} {1} x1".format(username, password))
573 | line_match = self.login_response_re.match(line)
574 | if line_match:
575 | if line_match.group(2) == " OK":
576 | if line_match.group(1) == username:
577 | return True
578 | elif line_match.group(1) == "incorrect":
579 | raise ValueError("Login failed. Check username and password.")
580 | raise ValueError("Login response was invalid.")
581 |
582 | def logout(self):
583 | line = self.command("LOGOUT")
584 | if line == "LOGOUT:completed":
585 | raise ValueError("Logout failed")
586 |
587 | def wait_match(self, block=True):
588 | while True:
589 | game_summary_str = self.read_game_summary(block)
590 | if game_summary_str is not None:
591 | return self.parse_game_summary(game_summary_str)
592 | else:
593 | return None
594 |
595 | def wait_server_message(self, board, block=True):
596 | while True:
597 | line = self.read_line(block)
598 | if line is None:
599 | return None
600 | return self.parse_server_message(line, board)
601 |
602 | def parse_server_message(self, line, board):
603 | if line[0] in COLOR_SYMBOLS:
604 | move_strs = line.split(",")
605 | move_str = move_strs[0]
606 | time_str = move_strs[1] if len(move_strs) > 1 else None
607 | (color, usi) = Parser.parse_move_str(move_str, board)
608 | return (color, usi, self.parse_consumed_time_str(time_str), None)
609 | elif line[0] in ["#", "%"]:
610 | message = SERVER_MESSAGE_SYMBOLS.index(line[1:])
611 | return (None, None, None, message)
612 | else:
613 | raise ValueError("Invalid lines")
614 |
615 | def parse_consumed_time_str(self, time_str):
616 | # This function always returns float seconds.
617 | # TODO: refer Time_Unit header.
618 | if time_str is None:
619 | return None
620 | elif time_str[0] != "T":
621 | raise ValueError("Invalid consumed time format")
622 | return float(time_str[1:])
623 |
624 | def read_game_summary(self, block=True):
625 | return self.read_until("END Game_Summary\n", block)
626 |
627 | def parse_game_summary(self, game_summary_block):
628 | time_lines = None
629 | position_lines = None
630 | names = [None, None]
631 | position = None
632 | for line in game_summary_block.split("\n"):
633 | if line == "BEGIN Game_Summary" or line == "END Game_Summary":
634 | pass
635 | elif line == "BEGIN Time":
636 | time_lines = []
637 | elif line == "END Time":
638 | time_summary = self.parse_time(time_lines)
639 | time_lines = None
640 | elif line == "BEGIN Position":
641 | position_lines = []
642 | elif line == "END Position":
643 | position = Parser.parse_position(position_lines)
644 | position_lines = None
645 | elif time_lines is not None:
646 | time_lines.append(line)
647 | elif position_lines is not None:
648 | position_lines.append(line)
649 | elif ":" in line:
650 | (key, value) = line.split(":", 1)
651 | if key == "Name+":
652 | # sente or shiatte
653 | names[shogi.BLACK] = value
654 | elif key == "Name-":
655 | # sente or shiatte
656 | names[shogi.WHITE] = value
657 | elif key == "To_Move":
658 | to_move_color_str = value
659 | elif key == "Your_Turn":
660 | my_color = COLOR_SYMBOLS.index(value)
661 | elif not line:
662 | # empty line
663 | pass
664 | else:
665 | raise ValueError("Invalid game summary line: {0}".format(line))
666 |
667 | sfen = Exporter.sfen(position["pieces"], position["pieces_in_hand"], to_move_color_str, 1)
668 |
669 | summary = {
670 | "names": names,
671 | "sfen": sfen,
672 | "moves": [],
673 | "time": time_summary,
674 | }
675 |
676 | return {
677 | "summary": summary,
678 | "my_color": my_color,
679 | }
680 |
681 | def parse_time(self, time_block_lines):
682 | time = {}
683 | for line in time_block_lines:
684 | (key, value) = line.split(":", 1)
685 | time[key] = value
686 | return time
687 |
688 | def agree(self):
689 | # TODO: check START:
690 | self.command("AGREE")
691 |
692 | def reject(self):
693 | # TODO: check REJECT: by
694 | self.command("REJECT")
695 |
696 | def move(self, piece_type, color, move):
697 | if move.from_square is None:
698 | from_square = "00"
699 | else:
700 | from_square = SQUARE_NAMES[move.from_square]
701 | command = "{0}{1}{2}{3}".format(
702 | COLOR_SYMBOLS[color], from_square, SQUARE_NAMES[move.to_square], PIECE_SYMBOLS[piece_type]
703 | )
704 | return self.command(command)
705 |
706 | def resign(self):
707 | # TODO: check RESIGN
708 | self.command("%TORYO")
709 | match_result = self.read_line() # noqa: F841
710 |
711 |
712 | class CSAHeartbeat(threading.Thread):
713 | def __init__(self, ping_target, sleep_duration, ping_duration):
714 | super(CSAHeartbeat, self).__init__()
715 | self.ping_timer = 0
716 | self.ping_target = ping_target
717 | self.sleep_duration = sleep_duration
718 | self.ping_duration = ping_duration
719 |
720 | def run(self):
721 | if self.ping_timer >= self.ping_duration:
722 | self.ping_target.ping()
723 |
724 | self.ping_timer += self.sleep_duration
725 | time.sleep(self.sleep_duration)
726 |
--------------------------------------------------------------------------------
/shogi/Consts.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | COLORS = [BLACK, WHITE] = range(2)
4 |
5 | PIECE_TYPES_WITH_NONE = [
6 | NONE,
7 | PAWN,
8 | LANCE,
9 | KNIGHT,
10 | SILVER,
11 | GOLD,
12 | BISHOP,
13 | ROOK,
14 | KING,
15 | PROM_PAWN,
16 | PROM_LANCE,
17 | PROM_KNIGHT,
18 | PROM_SILVER,
19 | PROM_BISHOP,
20 | PROM_ROOK,
21 | ] = range(15)
22 | PIECE_TYPES = [
23 | PAWN,
24 | LANCE,
25 | KNIGHT,
26 | SILVER,
27 | GOLD,
28 | BISHOP,
29 | ROOK,
30 | KING,
31 | PROM_PAWN,
32 | PROM_LANCE,
33 | PROM_KNIGHT,
34 | PROM_SILVER,
35 | PROM_BISHOP,
36 | PROM_ROOK,
37 | ]
38 |
--------------------------------------------------------------------------------
/shogi/KIF.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # This file is part of the python-shogi library.
4 | # Copyright (C) 2015- Tasuku SUENAGA
5 | #
6 | # This program is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, either version 3 of the License, or
9 | # (at your option) any later version.
10 | #
11 | # This program is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | # GNU General Public License for more details.
15 | #
16 | # You should have received a copy of the GNU General Public License
17 | # along with this program. If not, see .
18 |
19 | # NOTE: Don't support ki2(Kifu2) format
20 |
21 | import codecs
22 | import os
23 | import re
24 | import sys
25 |
26 | import shogi
27 |
28 | # python dict pre-version 3.7 is not guaranteed to be sorted by insertion.
29 | # The OrderedDict implementation fixes this for the older versions.
30 | if sys.version_info[0] == 3 and sys.version_info[1] < 7:
31 | from collections import OrderedDict as ordered_dict
32 | else:
33 | ordered_dict = dict
34 |
35 |
36 | class ParserException(Exception):
37 | pass
38 |
39 |
40 | class Parser:
41 | MOVE_RE = re.compile(
42 | r"\A *[0-9]+\s+(中断|投了|持将棋|先日手|詰み|切れ負け|反則勝ち|反則負け|(([123456789])([零一二三四五六七八九])|同 )([歩香桂銀金角飛玉と杏圭全馬龍])(打|(成?)\(([0-9])([0-9])\)))\s*(\([ /:0-9]+\))?\s*\Z"
43 | )
44 |
45 | HANDYCAP_SFENS = {
46 | "平手": shogi.STARTING_SFEN,
47 | "香落ち": "lnsgkgsn1/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL w - 1",
48 | "右香落ち": "1nsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL w - 1",
49 | "角落ち": "lnsgkgsnl/1r7/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL w - 1",
50 | "飛車落ち": "lnsgkgsnl/7b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL w - 1",
51 | "飛香落ち": "lnsgkgsn1/7b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL w - 1",
52 | "二枚落ち": "lnsgkgsnl/9/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL w - 1",
53 | "三枚落ち": "lnsgkgsn1/9/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL w - 1",
54 | "四枚落ち": "1nsgkgsn1/9/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL w - 1",
55 | "五枚落ち": "2sgkgsn1/9/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL w - 1",
56 | "左五枚落ち": "1nsgkgs2/9/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL w - 1",
57 | "六枚落ち": "2sgkgs2/9/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL w - 1",
58 | "八枚落ち": "3gkg3/9/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL w - 1",
59 | "十枚落ち": "4k4/9/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL w - 1",
60 | "その他": None,
61 | }
62 |
63 | RESULT_RE = re.compile(r" *まで(\d+)手で((先|下|後|上)手の勝ち|千日手|持将棋|中断)")
64 |
65 | @staticmethod
66 | def parse_file(path):
67 | prefix, ext = os.path.splitext(path)
68 | for enc in ["cp932", "utf-8-sig"]:
69 | try:
70 | with codecs.open(path, "r", enc) as f:
71 | return Parser.parse_str(f.read())
72 | except Exception:
73 | pass
74 | return None
75 |
76 | @staticmethod
77 | def parse_pieces_in_hand(target):
78 | if target == "なし": # None in japanese
79 | return ordered_dict()
80 |
81 | result = ordered_dict()
82 | for item in target.replace(" ", " ").split(" "):
83 | if len(item) == 1:
84 | result[shogi.PIECE_JAPANESE_SYMBOLS.index(item)] = 1
85 | elif len(item) == 2 or len(item) == 3:
86 | result[shogi.PIECE_JAPANESE_SYMBOLS.index(item[0])] = shogi.NUMBER_JAPANESE_KANJI_SYMBOLS.index(
87 | item[1:]
88 | )
89 | elif len(item) == 0:
90 | pass
91 | else:
92 | raise ParserException("Invalid pieces in hand")
93 | return result
94 |
95 | @staticmethod
96 | def parse_board_line(line):
97 | board_line = line.split("|")[1].replace(" ", "")
98 | line_sfen = ""
99 | square_skip = 0
100 | sente = True
101 |
102 | for square in board_line:
103 | # if there is a piece in the square (no dot)
104 | if square != "・":
105 | # if there is a square skip, add to sfen
106 | if square_skip > 0:
107 | line_sfen = "".join((line_sfen, str(square_skip)))
108 | square_skip = 0
109 |
110 | if square == "v":
111 | sente = False
112 | continue
113 |
114 | # get the piece roman symbol
115 | piece = shogi.PIECE_SYMBOLS[shogi.PIECE_JAPANESE_SYMBOLS.index(square[-1])]
116 |
117 | # if sente
118 | if sente:
119 | line_sfen = "".join((line_sfen, piece.upper()))
120 | else:
121 | line_sfen = "".join((line_sfen, piece.lower()))
122 | sente = True
123 | else:
124 | square_skip += 1
125 |
126 | # if last square is also empty, need to add the skip to the end
127 | if square_skip > 0:
128 | line_sfen = "".join((line_sfen, str(square_skip)))
129 |
130 | return line_sfen
131 |
132 | @staticmethod
133 | def complete_custom_sfen(board, pieces_in_hand, turn):
134 | if turn == shogi.BLACK:
135 | turn_str = "b"
136 | else:
137 | turn_str = "w"
138 |
139 | # add whos turn it is
140 | sfen = "".join((board, " ", turn_str, " "))
141 |
142 | # if there are pieces in the hand
143 | if sum(pieces_in_hand[shogi.BLACK].values()) + sum(pieces_in_hand[shogi.WHITE].values()):
144 | for key, quantity in pieces_in_hand[shogi.BLACK].items():
145 | piece = shogi.PIECE_SYMBOLS[key].upper()
146 | if quantity > 1:
147 | sfen = "".join((sfen, str(quantity), piece))
148 | elif quantity == 1:
149 | sfen = "".join((sfen, piece))
150 |
151 | for key, quantity in pieces_in_hand[shogi.WHITE].items():
152 | piece = shogi.PIECE_SYMBOLS[key].lower()
153 | sfen = "".join((sfen, str(quantity), piece))
154 | else:
155 | sfen = "".join((sfen, "-"))
156 |
157 | # add the initial move number
158 | sfen = "".join((sfen, " 1"))
159 |
160 | return sfen
161 |
162 | @staticmethod
163 | def parse_move_str(line, last_to_square):
164 | # Normalize king/promoted kanji
165 | line = line.replace("王", "玉")
166 | line = line.replace("竜", "龍")
167 | line = line.replace("成銀", "全")
168 | line = line.replace("成桂", "圭")
169 | line = line.replace("成香", "杏")
170 | line = line.replace("不成", "") # normalize explicit non-promotions
171 |
172 | m = Parser.MOVE_RE.match(line)
173 | if m:
174 | if m.group(1) in ["中断", "投了", "持将棋", "千日手", "詰み", "切れ負け", "反則勝ち", "反則負け"]:
175 | return (None, None, m.group(1))
176 |
177 | piece_type = shogi.PIECE_JAPANESE_SYMBOLS.index(m.group(5))
178 | if m.group(2) == "同 ":
179 | # same position
180 | to_square = last_to_square
181 | else:
182 | to_field = 9 - shogi.NUMBER_JAPANESE_NUMBER_SYMBOLS.index(m.group(3))
183 | to_rank = shogi.NUMBER_JAPANESE_KANJI_SYMBOLS.index(m.group(4)) - 1
184 | to_square = to_rank * 9 + to_field
185 | last_to_square = to_square
186 |
187 | if m.group(6) == "打":
188 | # piece drop
189 | return (
190 | "{0}*{1}".format(shogi.PIECE_SYMBOLS[piece_type].upper(), shogi.SQUARE_NAMES[to_square]),
191 | last_to_square,
192 | None,
193 | )
194 | else:
195 | from_field = 9 - int(m.group(8))
196 | from_rank = int(m.group(9)) - 1
197 | from_square = from_rank * 9 + from_field
198 |
199 | promotion = m.group(7) == "成"
200 | return (
201 | shogi.SQUARE_NAMES[from_square] + shogi.SQUARE_NAMES[to_square] + ("+" if promotion else ""),
202 | last_to_square,
203 | None,
204 | )
205 | return (None, last_to_square, None)
206 |
207 | @staticmethod
208 | def parse_str(kif_str): # noqa: C901
209 | line_no = 1
210 |
211 | names = [None, None]
212 | pieces_in_hand = [ordered_dict(), ordered_dict()]
213 | current_turn = shogi.BLACK
214 | sfen = shogi.STARTING_SFEN
215 | moves = []
216 | last_to_square = None
217 | win = None
218 | custom_sfen = False
219 | kif_str = kif_str.replace("\r\n", "\n").replace("\r", "\n")
220 | for line in kif_str.split("\n"):
221 | if len(line) == 0 or line[0] == "*":
222 | pass
223 | elif line.count("+") == 2 and line.count("-") > 10:
224 | if custom_sfen:
225 | custom_sfen = False
226 | # remove last slash
227 | sfen = sfen[:-1]
228 | else:
229 | custom_sfen = True
230 | sfen = ""
231 | elif custom_sfen:
232 | sfen = "".join((sfen, Parser.parse_board_line(line), "/"))
233 | elif ":" in line:
234 | (key, value) = line.split(":", 1)
235 | value = value.rstrip(" ")
236 | if key == "先手" or key == "下手": # sente or shitate
237 | # Blacks's name
238 | names[shogi.BLACK] = value
239 | elif key == "後手" or key == "上手": # gote or uwate
240 | # White's name
241 | names[shogi.WHITE] = value
242 | elif key == "先手の持駒" or key == "下手の持駒": # sente or shitate's pieces in hand
243 | # First player's pieces in hand
244 | pieces_in_hand[shogi.BLACK] = Parser.parse_pieces_in_hand(value)
245 | elif key == "後手の持駒" or key == "上手の持駒": # gote or uwate's pieces in hand
246 | # Second player's pieces in hand
247 | pieces_in_hand[shogi.WHITE] = Parser.parse_pieces_in_hand(value)
248 | elif key == "手合割": # teai wari
249 | sfen = Parser.HANDYCAP_SFENS[value]
250 | if sfen is None:
251 | raise ParserException('Cannot support handycap type "other"')
252 | elif line == "後手番":
253 | # Current turn is white
254 | current_turn = shogi.WHITE
255 | else:
256 | (move, last_to_square, special_str) = Parser.parse_move_str(line, last_to_square)
257 | if move is not None:
258 | moves.append(move)
259 | if current_turn == shogi.BLACK:
260 | current_turn = shogi.WHITE
261 | else: # current_turn == shogi.WHITE
262 | current_turn = shogi.BLACK
263 | elif special_str in ["投了", "詰み", "切れ負け", "反則負け"]:
264 | if current_turn == shogi.BLACK:
265 | win = "w"
266 | else: # current_turn == shogi.WHITE
267 | win = "b"
268 | elif special_str in ["反則勝ち", "入玉勝ち"]:
269 | if current_turn == shogi.BLACK:
270 | win = "b"
271 | else: # current_turn == shogi.WHITE
272 | win = "w"
273 | elif special_str in ["持将棋", "先日手"]:
274 | win = "-"
275 | else:
276 | m = Parser.RESULT_RE.match(line)
277 | if m:
278 | win_side_str = m.group(3)
279 | if win_side_str == "先" or win_side_str == "下":
280 | win = "b"
281 | elif win_side_str == "後" or win_side_str == "上":
282 | win = "w"
283 | else:
284 | # TODO: repetition of moves with continuous check
285 | win = "-"
286 | line_no += 1
287 |
288 | # if using a custom sfen
289 | if len(sfen.split(" ")) == 1:
290 | sfen = Parser.complete_custom_sfen(sfen, pieces_in_hand, current_turn)
291 |
292 | summary = {"names": names, "sfen": sfen, "moves": moves, "win": win}
293 |
294 | # NOTE: for the same interface with CSA parser
295 | return [summary]
296 |
297 |
298 | class ExporterException(Exception):
299 | pass
300 |
301 |
302 | class Exporter:
303 | FULL_WIDTH_NUMBER = "123456789"
304 | JAPANESE_NUMBER = "一二三四五六七八九"
305 |
306 | @staticmethod
307 | def kif(sfen_summary):
308 | names = sfen_summary["names"]
309 | kif = ""
310 | kif += "開始日時: \r\n"
311 | kif = kif + "終了日時: \r\n"
312 | kif += "手合割:平手\r\n"
313 | kif += f"先手:{names[shogi.BLACK]}\r\n"
314 | kif += f"後手:{names[shogi.WHITE]}\r\n"
315 | kif += "手数----指手---------消費時間-- \r\n"
316 |
317 | sfen = sfen_summary["sfen"]
318 | board = shogi.Board(sfen)
319 |
320 | moves = sfen_summary["moves"]
321 | for i, move in enumerate(moves):
322 | kif += f"{i+1} {Exporter.kif_move_from(move, board)} \r\n"
323 | board.push_usi(move)
324 | # NOTE: only resign.
325 | kif += f"{len(moves) + 1} 投了 \r\n"
326 | if len(moves) % 2 == 1:
327 | if sfen_summary["win"] == "w":
328 | raise ExporterException("Invalid win")
329 | win = "先手"
330 | else:
331 | if sfen_summary["win"] == "b":
332 | raise ExporterException("Invalid win")
333 | win = "後手"
334 | kif += f"まで{len(moves)}手で{win}の勝ち\r\n"
335 |
336 | return kif
337 |
338 | @staticmethod
339 | def kif_move_from(sfen_move, board):
340 | to_x = int(sfen_move[2])
341 | to_x_full_width_number = Exporter.FULL_WIDTH_NUMBER[to_x - 1]
342 | to_y = Exporter.number_from(sfen_move[3])
343 | to_y_japanese_number = Exporter.JAPANESE_NUMBER[to_y - 1]
344 | if sfen_move[1] == "*":
345 | # piece drop
346 | piece = shogi.PIECE_JAPANESE_SYMBOLS[shogi.PIECE_SYMBOLS.index(sfen_move[0].lower())]
347 | return f"{to_x_full_width_number}{to_y_japanese_number}{piece}打"
348 | # move piece on the board
349 | from_x = int(sfen_move[0])
350 | from_y = Exporter.number_from(sfen_move[1])
351 | piece_name = board.piece_at(9 - from_x + (from_y - 1) * 9).japanese_symbol()
352 | promoted = "成" if len(sfen_move) == 5 and sfen_move[4] == "+" else ""
353 | return f"{to_x_full_width_number}{to_y_japanese_number}{piece_name}{promoted}({from_x}{from_y})"
354 |
355 | @staticmethod
356 | def number_from(alphabet):
357 | return {"a": 1, "b": 2, "c": 3, "d": 4, "e": 5, "f": 6, "g": 7, "h": 8, "i": 9}[alphabet]
358 |
--------------------------------------------------------------------------------
/shogi/Move.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # NOTE: In chess we use "file - rank" notation like 'a1',
4 | # in shogi we use a number for file, an alphabet for rank
5 | # and opposite direction of files and ranks like '9i'.
6 | # We use chess style notation internally, but exports it with this table.
7 |
8 | from .Consts import NONE
9 | from .Piece import PIECE_SYMBOLS, Piece
10 |
11 | # fmt: off
12 | SQUARE_NAMES = [
13 | "9a", "8a", "7a", "6a", "5a", "4a", "3a", "2a", "1a",
14 | "9b", "8b", "7b", "6b", "5b", "4b", "3b", "2b", "1b",
15 | "9c", "8c", "7c", "6c", "5c", "4c", "3c", "2c", "1c",
16 | "9d", "8d", "7d", "6d", "5d", "4d", "3d", "2d", "1d",
17 | "9e", "8e", "7e", "6e", "5e", "4e", "3e", "2e", "1e",
18 | "9f", "8f", "7f", "6f", "5f", "4f", "3f", "2f", "1f",
19 | "9g", "8g", "7g", "6g", "5g", "4g", "3g", "2g", "1g",
20 | "9h", "8h", "7h", "6h", "5h", "4h", "3h", "2h", "1h",
21 | "9i", "8i", "7i", "6i", "5i", "4i", "3i", "2i", "1i",
22 | ]
23 | # fmt: on
24 |
25 |
26 | class Move(object):
27 | """
28 | Represents a move from a square to a square and possibly the promotion piece
29 | type.
30 | Null moves are supported.
31 | """
32 |
33 | def __init__(self, from_square, to_square, promotion=False, drop_piece_type=None):
34 | # if from_square is None, it's a drop and
35 | self.from_square = from_square
36 | self.to_square = to_square
37 | if from_square is None and to_square is not None:
38 | if drop_piece_type is None:
39 | raise ValueError("Drop piece type must be set.")
40 | if promotion:
41 | raise ValueError("Cannot set promoted piece.")
42 | self.promotion = False
43 | self.drop_piece_type = drop_piece_type
44 | else:
45 | self.promotion = promotion
46 | if drop_piece_type:
47 | raise ValueError("Drop piece type must not be set.")
48 | self.drop_piece_type = None
49 |
50 | def usi(self):
51 | """
52 | Gets an USI string for the move.
53 | For example a move from 7A to 8A would be `7a8a` or `7a8a+` if it is
54 | a promotion.
55 | """
56 | if self:
57 | if self.drop_piece_type:
58 | return "{0}*{1}".format(PIECE_SYMBOLS[self.drop_piece_type].upper(), SQUARE_NAMES[self.to_square])
59 | else:
60 | return SQUARE_NAMES[self.from_square] + SQUARE_NAMES[self.to_square] + ("+" if self.promotion else "")
61 | else:
62 | return "0000"
63 |
64 | def __bool__(self):
65 | return not bool(self.from_square is None and self.to_square is None)
66 |
67 | def __nonzero__(self):
68 | return (self.from_square or self.to_square) is not None
69 |
70 | def __eq__(self, other):
71 | try:
72 | return (
73 | self.from_square == other.from_square
74 | and self.to_square == other.to_square
75 | and self.promotion == other.promotion
76 | and self.drop_piece_type == other.drop_piece_type
77 | )
78 | except AttributeError:
79 | return False
80 |
81 | def __ne__(self, other):
82 | return not self.__eq__(other)
83 |
84 | def __repr__(self):
85 | return "Move.from_usi('{0}')".format(self.usi())
86 |
87 | def __str__(self):
88 | return self.usi()
89 |
90 | def __hash__(self):
91 | # 7 bit is enough to represent 81 patterns
92 | if self.drop_piece_type is None:
93 | return self.to_square | self.from_square << 7 | self.promotion << 14
94 | else:
95 | # drop piece
96 | return self.to_square | (81 + self.drop_piece_type) << 7
97 |
98 | @classmethod
99 | def from_usi(cls, usi):
100 | """
101 | Parses an USI string.
102 | Raises `ValueError` if the USI string is invalid.
103 | """
104 | if usi == "0000":
105 | return cls.null()
106 | elif len(usi) == 4:
107 | if usi[1] == "*":
108 | piece = Piece.from_symbol(usi[0])
109 | return cls(None, SQUARE_NAMES.index(usi[2:4]), False, piece.piece_type)
110 | else:
111 | return cls(SQUARE_NAMES.index(usi[0:2]), SQUARE_NAMES.index(usi[2:4]))
112 | elif len(usi) == 5 and usi[4] == "+":
113 | return cls(SQUARE_NAMES.index(usi[0:2]), SQUARE_NAMES.index(usi[2:4]), True)
114 | else:
115 | raise ValueError("expected usi string to be of length 4 or 5")
116 |
117 | @classmethod
118 | def null(cls):
119 | """
120 | Gets a null move.
121 | A null move just passes the turn to the other side (and possibly
122 | forfeits en-passant capturing). Null moves evaluate to `False` in
123 | boolean contexts.
124 | >>> bool(shogi.Move.null())
125 | False
126 | """
127 | return cls(None, None, NONE)
128 |
--------------------------------------------------------------------------------
/shogi/Person.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # This file is part of the python-shogi library.
4 | # Copyright (C) 2015- Tasuku SUENAGA
5 | #
6 | # This program is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, either version 3 of the License, or
9 | # (at your option) any later version.
10 | #
11 | # This program is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | # GNU General Public License for more details.
15 | #
16 | # You should have received a copy of the GNU General Public License
17 | # along with this program. If not, see .
18 |
19 | import re
20 |
21 | NAME_SUFFIX_RE = re.compile(
22 | r"(小\d|奨励会|アマ|さん|[四五六七八九]段|[一二三四五六]冠|前?(名人|棋聖|王位|王座|王将|棋王|九段|十段|竜王|nhk杯|新人王|女王|女流王座|女流名人|女流王将|女流王位|倉敷藤花)+)$"
23 | )
24 |
25 | NAMES_OF_PROFESSIONAL_PLAYERS = [
26 | # old
27 | "天野宗歩",
28 | "小野五平",
29 | "溝呂木光治",
30 | "小菅剣之助",
31 | "関根金次郎",
32 | "花田長太郎",
33 | "小林東伯斎",
34 | "阪田三吉",
35 | "坂田三吉",
36 | "木見金治郎",
37 | "井上義雄",
38 | "大崎熊雄",
39 | "宮松関三郎",
40 | "川井房郷",
41 | "石井秀吉",
42 | "長谷川清二郎",
43 | "神田辰之助",
44 | "土居市太郎",
45 | "山本樟郎",
46 | "小泉雅信",
47 | "平野信助",
48 | "藤内金吾",
49 | "飯塚勘一郎",
50 | "斎藤銀次郎",
51 | "鈴木禎一",
52 | "建部和歌夫",
53 | "上田三三",
54 | "畝美与吉",
55 | "松田辰雄",
56 | "大和久彪",
57 | "橋爪敏太郎",
58 | "野村慶虎",
59 | "村上真一",
60 | "金高清吉",
61 | "間宮純一",
62 | "藤川義夫",
63 | "京須行男",
64 | "松浦卓造",
65 | "山田道美",
66 | "市川伸",
67 | # shogi renmei
68 | "金易二郎",
69 | "木村義雄",
70 | "金子金五郎",
71 | "渡辺東一",
72 | "萩原淳",
73 | "中井捨吉",
74 | "大野源一",
75 | "志沢春吉",
76 | "市川一郎",
77 | "坂口允彦",
78 | "塚田正夫",
79 | "梶一郎",
80 | "角田三男",
81 | "加藤治郎",
82 | "松下力",
83 | "奥野基芳",
84 | "小堀清一",
85 | "升田幸三",
86 | "高島一岐代",
87 | "荒巻三之",
88 | "永沢勝雄",
89 | "加藤恵三",
90 | "岡崎史明",
91 | "北楯修哉",
92 | "松田茂役",
93 | "大山康晴",
94 | "山本武雄",
95 | "山中和正",
96 | "板谷四郎",
97 | "本間爽悦",
98 | "高柳敏夫",
99 | "廣津久雄",
100 | "吉田六彦",
101 | "富沢幹雄",
102 | "原田泰夫",
103 | "星田啓三",
104 | "南口繁一",
105 | "北村秀治郎",
106 | "花村元司",
107 | "山川次彦",
108 | "佐瀬勇次",
109 | "加藤博二",
110 | "丸田祐三",
111 | "灘蓮照",
112 | "平野広吉",
113 | "五十嵐豊一",
114 | "清野静男",
115 | "佐藤豊",
116 | "木川貴一",
117 | "下平幸男",
118 | "二見敬三",
119 | "西本馨",
120 | "神田鎮雄",
121 | "熊谷達人",
122 | "増田敏二",
123 | "浅沼一",
124 | "二上達也",
125 | "橋本三治",
126 | "津村常吉",
127 | "北村昌男",
128 | "関根茂",
129 | "大友昇",
130 | "佐藤庄平",
131 | "加藤一二三",
132 | "宮坂幸雄",
133 | "有吉道夫",
134 | "長谷部久雄",
135 | "芹沢博文",
136 | "関屋喜代作",
137 | "賀集正三",
138 | "大村和久",
139 | "大原英二",
140 | "剱持松二",
141 | "佐藤大五郎",
142 | "吉田利勝",
143 | "北村文男",
144 | "内藤國雄",
145 | "伊達康夫",
146 | "佐伯昌優",
147 | "木村嘉孝",
148 | "山口千嶺",
149 | "木村義徳",
150 | "高島弘光",
151 | "板谷進",
152 | "米長邦雄",
153 | "大内延介",
154 | "西村一義",
155 | "木下晃",
156 | "山口英夫",
157 | "桜井昇",
158 | "田辺一郎",
159 | "中原誠",
160 | "桐山清澄",
161 | "高田丈資",
162 | "河口俊彦",
163 | "勝浦修",
164 | "石田和雄",
165 | "若松政和",
166 | "森安秀光",
167 | "森雞二",
168 | "森けい二",
169 | "滝誠一郎",
170 | "池田修一",
171 | "野本虎次",
172 | "田中魁秀",
173 | "坪内利幸",
174 | "佐藤義則",
175 | "安恵照剛",
176 | "森安正幸",
177 | "田丸昇",
178 | "宮田利男",
179 | "真部一男",
180 | "小阪昇",
181 | "淡路仁茂",
182 | "青野照市",
183 | "椎橋金司",
184 | "前田祐司",
185 | "飯野健二",
186 | "伊藤果",
187 | "菊地常夫",
188 | "桐谷広人",
189 | "沼春雄",
190 | "有野芳人",
191 | "小林健二",
192 | "土佐浩司",
193 | "酒井順吉",
194 | "森信雄",
195 | "田中寅彦",
196 | "東和男",
197 | "中田章道",
198 | "大島映二",
199 | "谷川浩司",
200 | "松浦隆一",
201 | "青木清",
202 | "小野修一",
203 | "福崎文吾",
204 | "鈴木輝彦",
205 | "武者野勝巳",
206 | "脇謙二",
207 | "永作芳也",
208 | "瀬戸博晴",
209 | "児玉孝一",
210 | "高橋道雄",
211 | "中村修",
212 | "泉正樹",
213 | "依田有司",
214 | "島朗",
215 | "南芳一",
216 | "塚田泰明",
217 | "神谷広志",
218 | "植山悦行",
219 | "西川慶二",
220 | "武市三郎",
221 | "室岡克彦",
222 | "堀口弘治",
223 | "大野八一雄",
224 | "加瀬純一",
225 | "井上慶太",
226 | "有森浩三",
227 | "飯田弘之",
228 | "神吉宏充",
229 | "森下卓",
230 | "浦野真彦",
231 | "小野敦生",
232 | "日浦市郎",
233 | "達正光",
234 | "伊藤博文",
235 | "小林宏",
236 | "富岡英作",
237 | "関浩",
238 | "本間博",
239 | "阿部隆",
240 | "所司和晴",
241 | "中田宏樹",
242 | "安西勝一",
243 | "羽生善治",
244 | "中田功",
245 | "石川陽生",
246 | "長沼洋",
247 | "神崎健二",
248 | "村山聖",
249 | "櫛田陽一",
250 | "佐藤康光",
251 | "森内俊之",
252 | "中川大輔",
253 | "先崎学",
254 | "野田敬三",
255 | "木下浩一",
256 | "小倉久史",
257 | "屋敷伸之",
258 | "藤原直哉",
259 | "高田尚平",
260 | "畠山鎮",
261 | "畠山成幸",
262 | "丸山忠久",
263 | "郷田真隆",
264 | "佐藤秀司",
265 | "杉本昌隆",
266 | "藤井猛",
267 | "平藤眞吾",
268 | "豊川孝弘",
269 | "深浦康市",
270 | "真田圭一",
271 | "飯塚祐紀",
272 | "三浦弘行",
273 | "伊藤能",
274 | "川上猛",
275 | "久保利明",
276 | "行方尚史",
277 | "岡崎洋",
278 | "窪田義行",
279 | "北浜健介",
280 | "矢倉規広",
281 | "鈴木大介",
282 | "北島忠雄",
283 | "勝又清和",
284 | "松本佳介",
285 | "田村康介",
286 | "堀口一史座",
287 | "中座真",
288 | "近藤正和",
289 | "野月浩貴",
290 | "木村一基",
291 | "小林裕士",
292 | "佐藤紳哉",
293 | "増田裕司",
294 | "高野秀行",
295 | "山崎隆之",
296 | "伊奈祐介",
297 | "山本真也",
298 | "中尾敏之",
299 | "松尾歩",
300 | "金沢孝史",
301 | "阿久津主税",
302 | "安用寺孝功",
303 | "渡辺明",
304 | "飯島栄治",
305 | "千葉幸生",
306 | "上野裕和",
307 | "橋本崇載",
308 | "佐々木慎",
309 | "宮田敦史",
310 | "村田智弘",
311 | "大平武洋",
312 | "熊坂学",
313 | "藤倉勇樹",
314 | "横山泰明",
315 | "島本亮",
316 | "西尾明",
317 | "村山慈明",
318 | "佐藤和俊",
319 | "片上大輔",
320 | "中村亮介",
321 | "村中秀史",
322 | "阪口悟",
323 | "広瀬章人",
324 | "長岡裕也",
325 | "高崎一生",
326 | "遠山雄亮",
327 | "瀬川晶司",
328 | "糸谷哲郎",
329 | "中村太地",
330 | "戸辺誠",
331 | "佐藤天彦",
332 | "豊島将之",
333 | "金井恒太",
334 | "伊藤真吾",
335 | "村田顕弘",
336 | "及川拓馬",
337 | "稲葉陽",
338 | "田中悠一",
339 | "佐藤慎一",
340 | "西川和宏",
341 | "吉田正和",
342 | "澤田真吾",
343 | "大石直嗣",
344 | "永瀬拓矢",
345 | "阿部健治郎",
346 | "菅井竜也",
347 | "牧野光則",
348 | "佐々木勇気",
349 | "船江恒平",
350 | "門倉啓太",
351 | "阿部光瑠",
352 | "高見泰地",
353 | "藤森哲也",
354 | "斎藤慎太郎",
355 | "八代弥",
356 | "上村亘",
357 | "石田直裕",
358 | "渡辺大夢",
359 | "千田翔太",
360 | "竹内雄悟",
361 | "石井健太郎",
362 | "三枚堂達也",
363 | "星野良生",
364 | "宮本広志",
365 | "増田康宏",
366 | "黒沢怜生",
367 | "今泉健司",
368 | "青嶋未来",
369 | "梶浦宏孝",
370 | "高野智史",
371 | "近藤誠也",
372 | "都成竜馬",
373 | "井出隼平",
374 | "佐々木大地",
375 | "藤井聡太",
376 | "大橋貴洸",
377 | "西田拓也",
378 | "杉本和陽",
379 | ]
380 |
381 | NAMES_OF_LADIES_PROFESSIONAL_PLAYERS = [
382 | "蛸島彰子",
383 | "関根紀代子",
384 | "多田佳子",
385 | "山下カズ子",
386 | "寺下紀子",
387 | "村山幸子",
388 | "杉崎里子",
389 | "福崎睦美",
390 | "佐藤寿子",
391 | "谷川治恵",
392 | "森安多恵子",
393 | "宇治正子",
394 | "藤森奈津子",
395 | "長沢千和子",
396 | "神田真由美",
397 | "林葉直子",
398 | "中井広恵",
399 | "山田久美",
400 | "清水市代",
401 | "高群佐知子",
402 | "斎田晴子",
403 | "石高澄恵",
404 | "鹿野圭生",
405 | "植村真理",
406 | "船戸陽子",
407 | "大庭美樹",
408 | "真田彩子",
409 | "高橋和",
410 | "林まゆみ",
411 | "本田小百合",
412 | "久津知子",
413 | "矢内理絵子",
414 | "石橋幸緒",
415 | "中倉彰",
416 | "千葉涼子",
417 | "伊藤明日香",
418 | "竹部さゆり",
419 | "中倉宏美",
420 | "島井咲緒里",
421 | "早水千紗",
422 | "甲斐智美",
423 | "藤田麻衣子",
424 | "安食総子",
425 | "大庭美夏",
426 | "藤田綾",
427 | "上川香織",
428 | "野田澤彩乃",
429 | "山田朱未",
430 | "北尾まどか",
431 | "上田初美",
432 | "坂東香菜子",
433 | "村田智穂",
434 | "鈴木環那",
435 | "中村真梨花",
436 | "貞升南",
437 | "岩根忍",
438 | "里見香奈",
439 | "井道千尋",
440 | "室田伊緒",
441 | "伊奈川愛菓",
442 | "熊倉紫野",
443 | "中村桃子",
444 | "山口恵梨子",
445 | "香川愛生",
446 | "渡辺弥生",
447 | "室谷由紀",
448 | "長谷川優貴",
449 | "竹俣紅",
450 | "北村桂香",
451 | "相川春香",
452 | "飯野愛",
453 | "渡部愛",
454 | "山根ことみ",
455 | "和田あき",
456 | "塚田恵梨花",
457 | "伊藤沙恵",
458 | "中澤沙耶",
459 | "高浜愛子",
460 | "山口絵美菜",
461 | "里見咲紀",
462 | "石本さくら",
463 | "頼本奈菜",
464 | "カロリーナ・ステチェンスカ",
465 | "堀彩乃",
466 | ]
467 |
468 |
469 | class Name:
470 | @staticmethod
471 | def normalize(name):
472 | if name is None:
473 | return None
474 | name = name.translate(
475 | {
476 | ord(" "): None, # space
477 | ord(" "): None, # full-width space
478 | ord("・"): None, # full-width center dot
479 | }
480 | )
481 | name = NAME_SUFFIX_RE.sub("", name)
482 |
483 | return name
484 |
485 | @staticmethod
486 | def is_professional(name):
487 | normalized = Name.normalize(name)
488 | return normalized in NAMES_OF_PROFESSIONAL_PLAYERS
489 |
490 | @staticmethod
491 | def is_ladies_professional(name):
492 | normalized = Name.normalize(name)
493 | return normalized in NAMES_OF_LADIES_PROFESSIONAL_PLAYERS
494 |
--------------------------------------------------------------------------------
/shogi/Piece.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from .Consts import BLACK, PROM_PAWN, WHITE
4 |
5 | PIECE_SYMBOLS = ["", "p", "l", "n", "s", "g", "b", "r", "k", "+p", "+l", "+n", "+s", "+b", "+r"]
6 | PIECE_JAPANESE_SYMBOLS = ["", "歩", "香", "桂", "銀", "金", "角", "飛", "玉", "と", "杏", "圭", "全", "馬", "龍"]
7 |
8 |
9 | class Piece(object):
10 | def __init__(self, piece_type, color):
11 | if piece_type is None:
12 | raise ValueError("Piece type must be set")
13 | if color is None:
14 | raise ValueError("Color must be set")
15 | self.piece_type = piece_type
16 | self.color = color
17 |
18 | def symbol(self):
19 | """
20 | Gets the symbol `p`, `l`, `n`, etc.
21 | """
22 | if self.color == BLACK:
23 | return PIECE_SYMBOLS[self.piece_type].upper()
24 | else:
25 | return PIECE_SYMBOLS[self.piece_type]
26 |
27 | def japanese_symbol(self):
28 | # no direction
29 | return PIECE_JAPANESE_SYMBOLS[self.piece_type]
30 |
31 | def japanese_symbol_with_direction(self):
32 | if self.color == BLACK:
33 | prefix = " "
34 | else:
35 | prefix = "v"
36 | return prefix + PIECE_JAPANESE_SYMBOLS[self.piece_type]
37 |
38 | def is_promoted(self):
39 | return self.piece_type >= PROM_PAWN
40 |
41 | def __hash__(self):
42 | return self.piece_type * (self.color + 1)
43 |
44 | def __repr__(self):
45 | return "Piece.from_symbol('{0}')".format(self.symbol())
46 |
47 | def __str__(self):
48 | return self.symbol()
49 |
50 | def __eq__(self, other):
51 | try:
52 | return self.piece_type == other.piece_type and self.color == other.color
53 | except AttributeError:
54 | return False
55 |
56 | def __ne__(self, other):
57 | return not self.__eq__(other)
58 |
59 | @classmethod
60 | def from_symbol(cls, symbol):
61 | """
62 | Creates a piece instance from a piece symbol.
63 | Raises `ValueError` if the symbol is invalid.
64 | """
65 | if symbol.lower() == symbol:
66 | return cls(PIECE_SYMBOLS.index(symbol), WHITE)
67 | else:
68 | return cls(PIECE_SYMBOLS.index(symbol.lower()), BLACK)
69 |
--------------------------------------------------------------------------------
/tests/board_test.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # This file is part of the python-shogi library.
4 | # Copyright (C) 2015- Tasuku SUENAGA
5 | #
6 | # This program is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, either version 3 of the License, or
9 | # (at your option) any later version.
10 | #
11 | # This program is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | # GNU General Public License for more details.
15 | #
16 | # You should have received a copy of the GNU General Public License
17 | # along with this program. If not, see .
18 |
19 | import unittest
20 |
21 | import shogi
22 |
23 |
24 | class BoardTestCase(unittest.TestCase):
25 | def test_default(self):
26 | board_none = shogi.Board()
27 | board_sfen = shogi.Board(shogi.STARTING_SFEN)
28 | self.assertEqual(board_none, board_sfen)
29 | self.assertEqual(board_none.sfen(), shogi.STARTING_SFEN)
30 | self.assertEqual(str(board_none), str(board_sfen))
31 | self.assertEqual(repr(board_none), repr(board_sfen))
32 | self.assertEqual(board_none.turn, shogi.BLACK)
33 |
34 | def test_stalemate(self):
35 | board = shogi.Board("+R+N+SGKG+S+N+R/+B+N+SG+LG+S+N+B/P+LPP+LPP+LP/1P2P2P1/9/9/9/9/6k2 b - 200")
36 | self.assertEqual(len(board.pseudo_legal_moves), 0)
37 |
38 | def test_bishop_center(self):
39 | board = shogi.Board("9/9/9/9/4B4/9/9/9/9 b - 1")
40 | self.assertTrue(shogi.Move.from_usi("5e4d") in board.legal_moves)
41 | self.assertTrue(shogi.Move.from_usi("5e3c") in board.legal_moves)
42 | self.assertTrue(shogi.Move.from_usi("5e2b") in board.legal_moves)
43 | self.assertTrue(shogi.Move.from_usi("5e1a") in board.legal_moves)
44 | self.assertTrue(shogi.Move.from_usi("5e6f") in board.legal_moves)
45 | self.assertTrue(shogi.Move.from_usi("5e7g") in board.legal_moves)
46 | self.assertTrue(shogi.Move.from_usi("5e8h") in board.legal_moves)
47 | self.assertTrue(shogi.Move.from_usi("5e9i") in board.legal_moves)
48 | self.assertTrue(shogi.Move.from_usi("5e4f") in board.legal_moves)
49 | self.assertTrue(shogi.Move.from_usi("5e3g") in board.legal_moves)
50 | self.assertTrue(shogi.Move.from_usi("5e2h") in board.legal_moves)
51 | self.assertTrue(shogi.Move.from_usi("5e1i") in board.legal_moves)
52 | self.assertTrue(shogi.Move.from_usi("5e6d") in board.legal_moves)
53 | self.assertTrue(shogi.Move.from_usi("5e7c") in board.legal_moves)
54 | self.assertTrue(shogi.Move.from_usi("5e8b") in board.legal_moves)
55 | self.assertTrue(shogi.Move.from_usi("5e9a") in board.legal_moves)
56 |
57 | self.assertTrue(shogi.Move.from_usi("5e3c+") in board.legal_moves)
58 | self.assertTrue(shogi.Move.from_usi("5e2b+") in board.legal_moves)
59 | self.assertTrue(shogi.Move.from_usi("5e1a+") in board.legal_moves)
60 | self.assertTrue(shogi.Move.from_usi("5e7c+") in board.legal_moves)
61 | self.assertTrue(shogi.Move.from_usi("5e8b+") in board.legal_moves)
62 | self.assertTrue(shogi.Move.from_usi("5e9a+") in board.legal_moves)
63 |
64 | self.assertEqual(len(board.legal_moves), 22)
65 |
66 | def test_bishop_9a(self):
67 | board = shogi.Board("B8/9/9/9/9/9/9/9/9 b - 1")
68 | self.assertEqual(len(board.legal_moves), 16)
69 |
70 | def test_double_pawn_in_a_row(self):
71 | board = shogi.Board("k8/9/9/9/9/9/9/9/P8 b P 1")
72 | self.assertFalse(shogi.Move.from_usi("P*9b") in board.legal_moves)
73 | self.assertFalse(shogi.Move.from_usi("P*9c") in board.legal_moves)
74 | self.assertFalse(shogi.Move.from_usi("P*9d") in board.legal_moves)
75 | self.assertFalse(shogi.Move.from_usi("P*9e") in board.legal_moves)
76 | self.assertFalse(shogi.Move.from_usi("P*9f") in board.legal_moves)
77 | self.assertFalse(shogi.Move.from_usi("P*9g") in board.legal_moves)
78 | self.assertFalse(shogi.Move.from_usi("P*9h") in board.legal_moves)
79 | self.assertEqual(len(board.legal_moves), 65)
80 |
81 | def test_suicide(self):
82 | board = shogi.Board("1k7/9/1G7/9/9/9/9/9/9 w - 1")
83 | self.assertTrue(board.is_suicide_or_check_by_dropping_pawn(shogi.Move.from_usi("8a8b")))
84 | self.assertTrue(board.is_suicide_or_check_by_dropping_pawn(shogi.Move.from_usi("8a9b")))
85 | self.assertTrue(board.is_suicide_or_check_by_dropping_pawn(shogi.Move.from_usi("8a7b")))
86 | self.assertFalse(board.is_suicide_or_check_by_dropping_pawn(shogi.Move.from_usi("8a6b")))
87 | self.assertEqual(len(board.legal_moves), 2)
88 |
89 | def test_check_by_dropping_pawn(self):
90 | # check by dropping pawn
91 | board = shogi.Board("kn7/9/1G7/9/9/9/9/9/9 b P 1")
92 | self.assertTrue(board.is_suicide_or_check_by_dropping_pawn(shogi.Move.from_usi("P*9b")))
93 | self.assertEqual(len(board.legal_moves), 76)
94 | board = shogi.Board("kn7/9/9/1NN6/9/9/9/9/9 b P 1")
95 | self.assertTrue(board.is_suicide_or_check_by_dropping_pawn(shogi.Move.from_usi("P*9b")))
96 | self.assertEqual(len(board.legal_moves), 73)
97 |
98 | # king can escape
99 | board = shogi.Board("k8/9/9/9/9/9/9/9/9 b P 1")
100 | self.assertFalse(board.is_suicide_or_check_by_dropping_pawn(shogi.Move.from_usi("P*9b")))
101 | self.assertEqual(len(board.legal_moves), 72)
102 |
103 | # dropping pawn is not protected and king cannot escape
104 | board = shogi.Board("kn7/1n7/9/9/9/9/9/9/9 b P 1")
105 | self.assertFalse(board.is_suicide_or_check_by_dropping_pawn(shogi.Move.from_usi("P*9b")))
106 | self.assertEqual(len(board.legal_moves), 71)
107 |
108 | # dropping pawn is protected but king can escape
109 | board = shogi.Board("kn7/9/9/1N7/9/9/9/9/9 b P 1")
110 | self.assertFalse(board.is_suicide_or_check_by_dropping_pawn(shogi.Move.from_usi("P*9b")))
111 | self.assertEqual(len(board.legal_moves), 73)
112 | board = shogi.Board("k8/9/1S7/9/9/9/9/9/9 b P 1")
113 | self.assertFalse(board.is_suicide_or_check_by_dropping_pawn(shogi.Move.from_usi("P*9b")))
114 | self.assertEqual(len(board.legal_moves), 81)
115 |
116 | # dropping pawn can be captured other pieces besides king
117 | board = shogi.Board("kg7/9/1G7/9/9/9/9/9/9 b P 1")
118 | self.assertFalse(board.is_suicide_or_check_by_dropping_pawn(shogi.Move.from_usi("P*9b")))
119 | self.assertEqual(len(board.legal_moves), 77)
120 |
121 | def test_lance_move(self):
122 | board = shogi.Board("9/9/9/9/4L4/9/9/9/9 b - 1")
123 | self.assertEqual(len(board.legal_moves), 6)
124 |
125 | def test_is_fourfold_repetition(self):
126 | board = shogi.Board("ln3g2l/1r2g1sk1/1pp1ppn2/p2ps1ppp/1PP6/2GP4P/P1N1PPPP1/1R2S1SK1/L4G1NL w Bb 44")
127 | for move_str in [
128 | "9d9e",
129 | "8h6h",
130 | "8b6b",
131 | "6h8h",
132 | "6b8b",
133 | "8h6h",
134 | "8b6b",
135 | "6h8h",
136 | "6b8b",
137 | "8h6h",
138 | "8b6b",
139 | "6h8h",
140 | ]:
141 | board.push(shogi.Move.from_usi(move_str))
142 | self.assertFalse(board.is_fourfold_repetition())
143 | board.push(shogi.Move.from_usi("6b8b"))
144 | self.assertTrue(board.is_fourfold_repetition())
145 |
146 | def test_legal_moves_in(self):
147 | # https://github.com/gunyarakun/python-shogi/issues/3
148 | board = shogi.Board()
149 | self.assertTrue(shogi.Move.from_usi("9g9f") in board.legal_moves)
150 | self.assertTrue(shogi.Move.from_usi("8g8f") in board.legal_moves)
151 | self.assertTrue(shogi.Move.from_usi("7g7f") in board.legal_moves)
152 | self.assertTrue(shogi.Move.from_usi("6g6f") in board.legal_moves)
153 | self.assertTrue(shogi.Move.from_usi("5g5f") in board.legal_moves)
154 | self.assertTrue(shogi.Move.from_usi("4g4f") in board.legal_moves)
155 | self.assertTrue(shogi.Move.from_usi("3g3f") in board.legal_moves)
156 | self.assertTrue(shogi.Move.from_usi("2g2f") in board.legal_moves)
157 | self.assertTrue(shogi.Move.from_usi("1g1f") in board.legal_moves)
158 | self.assertTrue(shogi.Move.from_usi("9i9h") in board.legal_moves)
159 | self.assertTrue(shogi.Move.from_usi("1i1h") in board.legal_moves)
160 | self.assertTrue(shogi.Move.from_usi("7i7h") in board.legal_moves)
161 | self.assertTrue(shogi.Move.from_usi("7i6h") in board.legal_moves)
162 | self.assertTrue(shogi.Move.from_usi("3i4h") in board.legal_moves)
163 | self.assertTrue(shogi.Move.from_usi("3i3h") in board.legal_moves)
164 | self.assertTrue(shogi.Move.from_usi("6i7h") in board.legal_moves)
165 | self.assertTrue(shogi.Move.from_usi("6i6h") in board.legal_moves)
166 | self.assertTrue(shogi.Move.from_usi("6i5h") in board.legal_moves)
167 | self.assertTrue(shogi.Move.from_usi("4i5h") in board.legal_moves)
168 | self.assertTrue(shogi.Move.from_usi("4i4h") in board.legal_moves)
169 | self.assertTrue(shogi.Move.from_usi("4i3h") in board.legal_moves)
170 | self.assertTrue(shogi.Move.from_usi("2h7h") in board.legal_moves)
171 | self.assertTrue(shogi.Move.from_usi("2h6h") in board.legal_moves)
172 | self.assertTrue(shogi.Move.from_usi("2h5h") in board.legal_moves)
173 | self.assertTrue(shogi.Move.from_usi("2h4h") in board.legal_moves)
174 | self.assertTrue(shogi.Move.from_usi("2h3h") in board.legal_moves)
175 | self.assertTrue(shogi.Move.from_usi("2h1h") in board.legal_moves)
176 | self.assertTrue(shogi.Move.from_usi("5i6h") in board.legal_moves)
177 | self.assertTrue(shogi.Move.from_usi("5i5h") in board.legal_moves)
178 | self.assertTrue(shogi.Move.from_usi("5i4h") in board.legal_moves)
179 |
180 | # opposite turn
181 | self.assertFalse(shogi.Move.from_usi("9c9d") in board.legal_moves)
182 |
183 | def test_sfen_piece_in_hand_order(self):
184 | # ref: https://web.archive.org/web/20080131070731/http://www.glaurungchess.com/shogi/usi.html
185 | # Invalid sfen, but acceptable in python-shogi
186 | board = shogi.Board("4k4/9/9/9/9/9/9/9/4K4 b 9p2l2n2s2gbr9P2L2N2S2GBR 1")
187 | self.assertEqual(board.sfen(), "4k4/9/9/9/9/9/9/9/4K4 b RB2G2S2N2L9Prb2g2s2n2l9p 1")
188 |
189 | def test_issue_6(self):
190 | # double pawn should be checked for their own pawn
191 | board = shogi.Board("lr7/3skgg1+B/2n2s1pp/p1p1ppP2/3p1np2/1PPPP4/PS1G1P2P/2GS3R1/LNK4NL w L2pb 58")
192 | self.assertEqual(len(board.legal_moves), 92)
193 |
194 | def test_issue_7(self):
195 | # SQUARES_R45 was wrong
196 | board = shogi.Board("lnsg1g1nl/3k3r1/pppp1s1pp/b3p1p2/2PP1p2B/P3P3P/1P3PPP1/1S3K1R1/LN1G1GSNL w - 1")
197 | self.assertEqual(len(board.legal_moves), 39)
198 |
199 | def test_issue_9(self):
200 | self.assertEqual(bool(shogi.Move.null()), False)
201 | board = shogi.Board()
202 | board.push(shogi.Move.null())
203 | self.assertEqual(board.captured_piece_stack[0], 0)
204 |
205 | def test_issue_15(self):
206 | # Issue: All moves from "9a" are not pseudo legal.
207 | board = shogi.Board()
208 | # pass turn to white
209 | board.push(shogi.Move.from_usi("9h9g"))
210 | move = shogi.Move.from_usi("9a9b")
211 | self.assertEqual(board.is_pseudo_legal(move), True)
212 |
213 | def test_issue_17(self):
214 | board = shogi.Board("ln1g3+Rl/1ks4s1/pp1gppbpp/2p3N2/9/5P1P1/PPPP1S1bP/2K1R1G2/LNSG3NL w 4p 42")
215 | move = shogi.Move.from_usi("2g5d+")
216 | self.assertTrue(move in board.legal_moves)
217 |
218 | def test_usi_command(self):
219 | board = shogi.Board()
220 |
221 | board.push_usi_position_cmd("position startpos moves 7g7f")
222 | self.assertEqual(board.sfen(), "lnsgkgsnl/1r5b1/ppppppppp/9/9/2P6/PP1PPPPPP/1B5R1/LNSGKGSNL w - 2")
223 | board.push_usi_position_cmd(
224 | "position sfen ln1g3+Rl/1ks4s1/pp1gppbpp/2p3N2/9/5P1P1/PPPP1S1bP/2K1R1G2/LNSG3NL w 4p 42"
225 | )
226 | move = shogi.Move.from_usi("2g5d+")
227 | self.assertTrue(move in board.legal_moves)
228 | board.push_usi_position_cmd(
229 | "position sfen ln1g3+Rl/1ks4s1/pp1gppbpp/2p3N2/9/5P1P1/PPPP1S1bP/2K1R1G2/LNSG3NL w 4p 42 moves 2g5d+"
230 | )
231 | self.assertEqual(board.sfen(), "ln1g3+Rl/1ks4s1/pp1gppbpp/2p1+b1N2/9/5P1P1/PPPP1S2P/2K1R1G2/LNSG3NL b 4p 43")
232 |
233 | with self.assertRaises(ValueError):
234 | board.push_usi_position_cmd("position moves")
235 |
236 |
237 | if __name__ == "__main__":
238 | unittest.main()
239 |
--------------------------------------------------------------------------------
/tests/csa_test.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # This file is part of the python-shogi library.
4 | # Copyright (C) 2015- Tasuku SUENAGA
5 | #
6 | # This program is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, either version 3 of the License, or
9 | # (at your option) any later version.
10 | #
11 | # This program is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | # GNU General Public License for more details.
15 | #
16 | # You should have received a copy of the GNU General Public License
17 | # along with this program. If not, see .
18 |
19 | # flake8: noqa W291
20 |
21 | import unittest
22 |
23 | from mock import patch
24 |
25 | import shogi
26 | from shogi import CSA
27 |
28 | TEST_CSA = """'----------棋譜ファイルの例"example.csa"-----------------
29 | 'バージョン
30 | V2.2
31 | '対局者名
32 | N+NAKAHARA
33 | N-YONENAGA
34 | '棋譜情報
35 | '棋戦名
36 | $EVENT:13th World Computer Shogi Championship
37 | '対局場所
38 | $SITE:KAZUSA ARC
39 | '開始日時
40 | $START_TIME:2003/05/03 10:30:00
41 | '終了日時
42 | $END_TIME:2003/05/03 11:11:05
43 | '持ち時間:25分、切れ負け
44 | $TIME_LIMIT:00:25+00
45 | '戦型:矢倉
46 | $OPENING:YAGURA
47 | '平手の局面
48 | P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
49 | P2 * -HI * * * * * -KA *
50 | P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
51 | P4 * * * * * * * * *
52 | P5 * * * * * * * * *
53 | P6 * * * * * * * * *
54 | P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
55 | P8 * +KA * * * * * +HI *
56 | P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
57 | '先手番
58 | +
59 | '指し手と消費時間(optional)
60 | +2726FU
61 | T12
62 | -3334FU
63 | T6
64 | +7776FU
65 | %TORYO
66 | '---------------------------------------------------------
67 | """
68 |
69 | TEST_CSA_SUMMARY = {
70 | "moves": ["2g2f", "3c3d", "7g7f"],
71 | "sfen": "lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL b - 1",
72 | "names": ["NAKAHARA", "YONENAGA"],
73 | "win": "b",
74 | }
75 |
76 | TEST_CSA_WITH_PI = """
77 | V2.2
78 | N+先手
79 | N-後手
80 | $START_TIME:2020/05/04 12:40:52
81 | PI82HI22KA
82 | +
83 | +7776FU
84 | T1
85 | -8384FU
86 | T11
87 | %TORYO
88 | T0
89 | """
90 |
91 | TEST_CSA_SUMMARY_WITH_PI = {
92 | "moves": ["7g7f", "8c8d"],
93 | "sfen": "lnsgkgsnl/9/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL b - 1",
94 | "names": ["先手", "後手"],
95 | "win": "w",
96 | }
97 |
98 |
99 | class ParserTest(unittest.TestCase):
100 | def parse_str_test(self):
101 | result = CSA.Parser.parse_str(TEST_CSA)
102 | self.assertEqual(result[0], TEST_CSA_SUMMARY)
103 |
104 | def parse_str_test_with_PI(self):
105 | result = CSA.Parser.parse_str(TEST_CSA_WITH_PI)
106 | self.assertEqual(result[0], TEST_CSA_SUMMARY_WITH_PI)
107 |
108 |
109 | TEST_SUMMARY = {
110 | "names": ["kiki_no_onaka_black", "kiki_no_omata_white"],
111 | "sfen": "lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL b - 1",
112 | "moves": [],
113 | "time": {"Time_Unit": "1sec", "Total_Time": "900", "Byoyomi": "0", "Least_Time_Per_Move": "1"},
114 | }
115 |
116 | TEST_SUMMARY_STR = """BEGIN Game_Summary
117 | Protocol_Version:1.1
118 | Protocol_Mode:Server
119 | Format:Shogi 1.0
120 | Declaration:Jishogi 1.1
121 | Game_ID:20150505-CSA25-3-5-7
122 | Name+:kiki_no_onaka_black
123 | Name-:kiki_no_omata_white
124 | Your_Turn:-
125 | Rematch_On_Draw:NO
126 | To_Move:+
127 | Max_Moves:123
128 | BEGIN Time
129 | Time_Unit:1sec
130 | Total_Time:900
131 | Byoyomi:0
132 | Least_Time_Per_Move:1
133 | END Time
134 | BEGIN Position
135 | P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
136 | P2 * -HI * * * * * -KA *
137 | P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
138 | P4 * * * * * * * * *
139 | P5 * * * * * * * * *
140 | P6 * * * * * * * * *
141 | P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
142 | P8 * +KA * * * * * +HI *
143 | P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
144 | +
145 | END Position
146 | END Game_Summary
147 | """
148 |
149 |
150 | class TCPProtocolTest(unittest.TestCase):
151 | def setUp(self):
152 | patchers = []
153 | patchers.append(patch.object(CSA.TCPProtocol, "connect", return_value=None))
154 | patchers.append(patch.object(CSA.TCPProtocol, "write"))
155 | patchers.append(patch.object(CSA.TCPProtocol, "read", return_value=0))
156 | for patcher in patchers:
157 | self.addCleanup(patcher.stop)
158 | patcher.start()
159 |
160 | self.maxDiff = None
161 |
162 | def add_response(self, csa_protocol, response):
163 | csa_protocol.recv_buf += response
164 |
165 | def test_login(self):
166 | tcp = CSA.TCPProtocol("127.0.0.1")
167 | self.add_response(tcp, "LOGIN:python-syogi OK\n")
168 | login_result = tcp.login("python-syogi", "password")
169 | self.assertTrue(login_result)
170 |
171 | def test_fail_login(self):
172 | tcp = CSA.TCPProtocol("127.0.0.1")
173 | self.add_response(tcp, "LOGIN:incorrect\n")
174 | with self.assertRaises(ValueError):
175 | tcp.login("python-syogi", "password")
176 |
177 | def test_wait_match(self):
178 | tcp = CSA.TCPProtocol("127.0.0.1")
179 | self.add_response(tcp, TEST_SUMMARY_STR)
180 | game_summary = tcp.wait_match()
181 | self.assertEqual(game_summary, {"summary": TEST_SUMMARY, "my_color": shogi.WHITE})
182 |
183 | def test_match(self):
184 | tcp = CSA.TCPProtocol("127.0.0.1")
185 | self.add_response(tcp, "LOGIN:username OK\n")
186 | tcp.login("username", "password")
187 | self.add_response(tcp, TEST_SUMMARY_STR)
188 | game_summary = tcp.wait_match()
189 |
190 | board = shogi.Board(game_summary["summary"]["sfen"])
191 | self.add_response(tcp, "START:20150505-CSA25-3-5-7\n")
192 | tcp.agree()
193 |
194 | self.add_response(tcp, "+5756FU,T1\n")
195 | (turn, usi, spend_time, message) = tcp.wait_server_message(board)
196 | board.push(shogi.Move.from_usi(usi))
197 | self.assertEqual(turn, shogi.BLACK)
198 | self.assertEqual(spend_time, 1.0)
199 |
200 | self.assertEqual(board.sfen(), "lnsgkgsnl/1r5b1/ppppppppp/9/9/4P4/PPPP1PPPP/1B5R1/LNSGKGSNL w - 2")
201 |
202 | next_move = shogi.Move.from_usi("8c8d")
203 | board.push(next_move)
204 | self.add_response(tcp, "-8384FU,T2\n")
205 | response_line = tcp.move(board.pieces[next_move.to_square], shogi.WHITE, next_move)
206 | (turn, usi, spend_time, message) = tcp.parse_server_message(response_line, board)
207 | self.assertEqual(turn, shogi.WHITE)
208 | self.assertEqual(spend_time, 2.0)
209 |
210 | # without spent time ex.) Shogidokoro
211 | self.add_response(tcp, "+5655FU\n")
212 | (turn, usi, spend_time, message) = tcp.wait_server_message(board)
213 | board.push(shogi.Move.from_usi(usi))
214 | self.assertEqual(turn, shogi.BLACK)
215 | self.assertEqual(spend_time, None)
216 |
217 |
218 | if __name__ == "__main__":
219 | unittest.main()
220 |
--------------------------------------------------------------------------------
/tests/kif_test.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # This file is part of the python-shogi library.
4 | # Copyright (C) 2015- Tasuku SUENAGA
5 | #
6 | # This program is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, either version 3 of the License, or
9 | # (at your option) any later version.
10 | #
11 | # This program is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | # GNU General Public License for more details.
15 | #
16 | # You should have received a copy of the GNU General Public License
17 | # along with this program. If not, see .
18 |
19 | import codecs
20 | import os
21 | import shutil
22 | import tempfile
23 | import unittest
24 |
25 | from shogi import KIF
26 |
27 | TEST_KIF_STR = """開始日時:2006/12/15 21:03\r
28 | 消費時間:▲359△359\r
29 | 棋戦:順位戦\r
30 | 戦型:四間飛車\r
31 | \r
32 | 場所:東京「将棋会館」\r
33 | \r
34 | 持ち時間:6時間\r
35 | \r
36 | 手合割:平手\r
37 | \r
38 | 後手:藤井猛\r
39 | 先手:羽生善治\r
40 | 手数----指手---------消費時間--\r
41 | *棋戦詳細:第65期順位戦A級06回戦\r
42 | *「羽生善治王将」vs「藤井 猛九段」\r
43 | 1 7六歩(77) \r
44 | 2 3四歩(33) \r
45 | 3 2六歩(27) \r
46 | 4 4四歩(43) \r
47 | 5 4八銀(39) \r
48 | 6 4二飛(82) \r
49 | 7 6八王(59) \r
50 | 8 6二王(51) \r
51 | 9 7八王(68) \r
52 | 10 7二王(62) \r
53 | 11 5六歩(57) \r
54 | 12 3二銀(31) \r
55 | 13 5七銀(48) \r
56 | 14 4三銀(32) \r
57 | 15 7七角(88) \r
58 | 16 8二王(72) \r
59 | 17 2五歩(26) \r
60 | 18 3三角(22) \r
61 | 19 8八王(78) \r
62 | 20 5四銀(43) \r
63 | 21 6六歩(67) \r
64 | 22 9二香(91) \r
65 | 23 9八香(99) \r
66 | 24 9一王(82) \r
67 | 25 9九王(88) \r
68 | 26 8二銀(71) \r
69 | 27 8八銀(79) \r
70 | 28 7一金(61) \r
71 | 29 5八金(49) \r
72 | 30 7四歩(73) \r
73 | 31 6八金(58) \r
74 | 32 5二金(41) \r
75 | 33 9六歩(97) \r
76 | 34 9四歩(93) \r
77 | 35 7九金(69) \r
78 | 36 6二金(52) \r
79 | 37 7八金(68) \r
80 | 38 1四歩(13) \r
81 | 39 1六歩(17) \r
82 | 40 6四歩(63) \r
83 | 41 2六飛(28) \r
84 | 42 4五歩(44) \r
85 | 43 3六飛(26) \r
86 | 44 4三銀(54) \r
87 | 45 6八銀(57) \r
88 | 46 7二金(62) \r
89 | 47 8六歩(87) \r
90 | 48 5四歩(53) \r
91 | 49 6五歩(66) \r
92 | 50 同 歩(64) \r
93 | 51 3三角成(77) \r
94 | 52 同 桂(21) \r
95 | 53 3一角打 \r
96 | 54 4一飛(42) \r
97 | 55 6四角成(31) \r
98 | 56 7三銀(82) \r
99 | 57 6五馬(64) \r
100 | 58 6一飛(41) \r
101 | 59 6四歩打 \r
102 | 60 3九角打 \r
103 | 61 5五歩(56) \r
104 | 62 6四銀(73) \r
105 | 63 7四馬(65) \r
106 | 64 7三金(72) \r
107 | 65 同 馬(74) \r
108 | 66 同 銀(64) \r
109 | 67 5四歩(55) \r
110 | 68 5二歩打 \r
111 | 69 6七歩打 \r
112 | 70 6三飛(61) \r
113 | 71 1七桂(29) \r
114 | 72 9三角成(39) \r
115 | 73 5六飛(36) \r
116 | 74 4四銀(43) \r
117 | 75 2四歩(25) \r
118 | 76 同 歩(23) \r
119 | 77 2六飛(56) \r
120 | 78 3五銀(44) \r
121 | 79 5六飛(26) \r
122 | 80 7四角打 \r
123 | 81 5三歩成(54) \r
124 | 82 同 歩(52) \r
125 | 83 6六飛(56) \r
126 | 84 6四歩打 \r
127 | 85 7五歩(76) \r
128 | 86 4七角成(74) \r
129 | 87 7七銀(68) \r
130 | 88 7四歩打 \r
131 | 89 8五歩(86) \r
132 | 90 7五歩(74) \r
133 | 91 5二歩打 \r
134 | 92 6一金(71) \r
135 | 93 8七銀(88) \r
136 | 94 5二金(61) \r
137 | 95 8八金(79) \r
138 | 96 6二金(52) \r
139 | 97 2三金打 \r
140 | 98 5四歩(53) \r
141 | 99 3六歩(37) \r
142 | 100 4四銀(35) \r
143 | 101 2四金(23) \r
144 | 102 7四銀(73) \r
145 | 103 9五歩(96) \r
146 | 104 同 歩(94) \r
147 | 105 3五歩(36) \r
148 | 106 4六歩(45) \r
149 | 107 3四歩(35) \r
150 | 108 4五桂(33) \r
151 | 109 3三歩成(34) \r
152 | 110 8四歩(83) \r
153 | 111 4八歩打 \r
154 | 112 3六馬(47) \r
155 | 113 8六銀(77) \r
156 | 114 8二馬(93) \r
157 | 115 8四歩(85) \r
158 | 116 8五歩打 \r
159 | 117 9五銀(86) \r
160 | 118 同 香(92) \r
161 | 119 同 香(98) \r
162 | 120 9四歩打 \r
163 | 121 同 香(95) \r
164 | 122 9三歩打 \r
165 | 123 8三香打 \r
166 | 124 同 銀(74) \r
167 | 125 同 歩成(84) \r
168 | 126 同 馬(82) \r
169 | 127 9三香成(94) \r
170 | 128 同 桂(81) \r
171 | 129 9四歩打 \r
172 | 130 9五香打 \r
173 | 131 9六銀(87) \r
174 | 132 9八歩打 \r
175 | 133 同 王(99) \r
176 | 134 9六香(95) \r
177 | 135 同 飛(66) \r
178 | 136 9五歩打 \r
179 | 137 同 飛(96) \r
180 | 138 9二歩打 \r
181 | 139 9三歩成(94) \r
182 | 140 同 歩(92) \r
183 | 141 8五飛(95) \r
184 | 142 8四香打 \r
185 | 143 同 飛(85) \r
186 | 144 同 馬(83) \r
187 | 145 8七香打 \r
188 | 146 8五歩打 \r
189 | 147 9六桂打 \r
190 | 148 7四馬(84) \r
191 | 149 8四香打 \r
192 | 150 7二金(62) \r
193 | 151 9二歩打 \r
194 | 152 同 馬(74) \r
195 | 153 8三歩打 \r
196 | 154 9五銀打 \r
197 | 155 8二銀打 \r
198 | 156 同 金(72) \r
199 | 157 同 歩成(83) \r
200 | 158 同 馬(92) \r
201 | 159 同 香成(84) \r
202 | 160 同 王(91) \r
203 | 161 7四金打 \r
204 | 162 7二銀打 \r
205 | 163 8三歩打 \r
206 | 164 同 飛(63) \r
207 | 165 8四歩打 \r
208 | 166 5三飛(83) \r
209 | 167 7三歩打 \r
210 | 168 同 飛(53) \r
211 | 169 9一角打 \r
212 | 170 同 王(82) \r
213 | 171 7三金(74) \r
214 | 172 同 銀(72) \r
215 | 173 8三歩成(84) \r
216 | 174 8一金打 \r
217 | 175 9二歩打 \r
218 | 176 同 金(81) \r
219 | 177 7一飛打 \r
220 | 178 8一香打 \r
221 | 179 9二と(83) \r
222 | 180 同 王(91) \r
223 | 181 7二飛成(71) \r
224 | 182 投了 \r
225 | まで181手で先手の勝ち\r
226 | """
227 |
228 | TEST_KIF_STR_WITH_TIME = """# --- Kifu for Windows (HTTP) V6.54 棋譜ファイル ---
229 | 対局ID:1234\r
230 | 開始日時:2013/08/08 09:00\r
231 | 終了日時:2013/08/09 17:40\r
232 | 表題:王位戦\r
233 | 棋戦:第54期王位戦七番勝負 第4局\r
234 | 持ち時間:各8時間\r
235 | 消費時間:78▲452△442\r
236 | 場所:ホテル日航福岡\r
237 | 図:投了\r
238 | 手合割:平手 \r
239 | 先手:行方尚史\r
240 | 後手:羽生善治\r
241 | 先手省略名:行方\r
242 | 後手省略名:羽生\r
243 | 手数----指手---------消費時間--\r
244 | *ホテルアイネにて\r
245 | *\r
246 | *コメントは複数行あるよ\r
247 | 1 7六歩(77) ( 0:00/00:00:00)\r
248 | *手の間にもコメントはある。\r
249 | *こんな感じ\r
250 | 2 3四歩(33) ( 0:00/00:00:00)\r
251 | 3 2六歩(27) ( 0:00/00:00:00)\r
252 | 4 8四歩(83) ( 0:00/00:00:00)\r
253 | 5 2五歩(26) ( 0:00/00:00:00)\r
254 | 6 8五歩(84) ( 0:00/00:00:00)\r
255 | 7 7八金(69) ( 0:00/00:00:00)\r
256 | 8 3二金(41) ( 0:00/00:00:00)\r
257 | 9 2四歩(25) ( 0:00/00:00:00)\r
258 | 10 同 歩(23) ( 0:00/00:00:00)\r
259 | 11 同 飛(28) ( 0:00/00:00:00)\r
260 | 12 8六歩(85) ( 0:00/00:00:00)\r
261 | 13 同 歩(87) ( 0:00/00:00:00)\r
262 | 14 同 飛(82) ( 0:00/00:00:00)\r
263 | 15 3四飛(24) ( 0:00/00:00:00)\r
264 | 16 3三角(22) ( 0:00/00:00:00)\r
265 | 17 3六飛(34) ( 0:00/00:00:00)\r
266 | 18 8四飛(86) ( 0:00/00:00:00)\r
267 | 19 2六飛(36) ( 0:00/00:00:00)\r
268 | 20 2二銀(31) ( 0:00/00:00:00)\r
269 | 21 8七歩打 ( 0:00/00:00:00)\r
270 | 22 5二玉(51) ( 0:00/00:00:00)\r
271 | 23 4八銀(39) ( 0:00/00:00:00)\r
272 | 24 1四歩(13) ( 0:00/00:00:00)\r
273 | 25 1六歩(17) ( 0:00/00:00:00)\r
274 | 26 2三銀(22) ( 0:00/00:00:00)\r
275 | 27 5八玉(59) ( 0:00/00:00:00)\r
276 | 28 5一金(61) ( 0:00/00:00:00)\r
277 | 29 3八金(49) ( 0:00/00:00:00)\r
278 | 30 6二銀(71) ( 0:00/00:00:00)\r
279 | 31 7七角(88) ( 0:00/00:00:00)\r
280 | 32 9四歩(93) ( 0:00/00:00:00)\r
281 | 33 9六歩(97) ( 0:00/00:00:00)\r
282 | 34 7七角成(33) ( 0:00/00:00:00)\r
283 | 35 同 桂(89) ( 0:00/00:00:00)\r
284 | 36 3三桂(21) ( 0:00/00:00:00)\r
285 | 37 6八銀(79) ( 0:00/00:00:00)\r
286 | 38 9三桂(81) ( 0:00/00:00:00)\r
287 | 39 7五歩(76) ( 0:00/00:00:00)\r
288 | 40 2四歩打 ( 0:00/00:00:00)\r
289 | 41 7四歩(75) ( 0:00/00:00:00)\r
290 | 42 同 歩(73) ( 0:00/00:00:00)\r
291 | 43 7二歩打 ( 0:00/00:00:00)\r
292 | 44 6一金(51) ( 0:00/00:00:00)\r
293 | 45 8六飛(26) ( 0:00/00:00:00)\r
294 | *昼食休憩へ。\r
295 | *おなかすいた。\r
296 | 46 同 飛(84) ( 0:00/00:00:00)\r
297 | 47 同 歩(87) ( 0:00/00:00:00)\r
298 | 48 8九飛打 ( 0:00/00:00:00)\r
299 | 49 7九金(78) ( 0:00/00:00:00)\r
300 | 50 9九飛成(89) ( 0:00/00:00:00)\r
301 | 51 8一飛打 ( 0:00/00:00:00)\r
302 | 52 7五香打 ( 0:00/00:00:00)\r
303 | 53 4一角打 ( 0:00/00:00:00)\r
304 | *午後のおやつは、ミルクレープ。\r
305 | 54 5一玉(52) ( 0:00/00:00:00)\r
306 | 55 5九銀(48) ( 0:00/00:00:00)\r
307 | 56 7七香成(75) ( 0:00/00:00:00)\r
308 | 57 同 銀(68) ( 0:00/00:00:00)\r
309 | 58 7九龍(99) ( 0:00/00:00:00)\r
310 | 59 6八銀(77) ( 0:00/00:00:00)\r
311 | 60 9九龍(79) ( 0:00/00:00:00)\r
312 | 61 5二香打 ( 0:00/00:00:00)\r
313 | 62 4二玉(51) ( 0:00/00:00:00)\r
314 | 63 6一飛成(81) ( 0:00/00:00:00)\r
315 | 64 7六桂打 ( 0:00/00:00:00)\r
316 | 65 7九金打 ( 0:00/00:00:00)\r
317 | 66 6八桂成(76) ( 0:00/00:00:00)\r
318 | 67 同 金(79) ( 0:00/00:00:00)\r
319 | 68 4五桂(33) ( 0:00/00:00:00)\r
320 | 69 6二龍(61) ( 0:00/00:00:00)\r
321 | 70 8四角打 ( 0:00/00:00:00)\r
322 | 71 5一龍(62) ( 0:00/00:00:00)\r
323 | 72 同 角(84) ( 0:00/00:00:00)\r
324 | 73 同 香成(52) ( 0:00/00:00:00)\r
325 | 74 5七桂成(45) ( 0:00/00:00:00)\r
326 | 75 同 金(68) ( 0:00/00:00:00)\r
327 | 76 5九龍(99) ( 0:00/00:00:00)\r
328 | 77 同 玉(58) ( 0:00/00:00:00)\r
329 | 78 7九飛打 ( 0:00/00:00:00)\r
330 | *この手にて投了。\r
331 | 79 投了 ( 0:00/00:00:00)\r
332 | まで78手で後手の勝ち\r
333 | """
334 |
335 | TEST_KIF_81DOJO = """#KIF version=2.0 encoding=UTF-8\r
336 | 開始日時:2020/12/31\r
337 | 場所:81Dojo\r
338 | 持ち時間:0分+10秒\r
339 | 手合割:平手\r
340 | 先手:KikiNoOmata\r
341 | 後手:XiaoNoOmata\r
342 | 手数----指手---------消費時間--\r
343 | 1 7六歩(77) (0:2/0:0:2)\r
344 | 2 3四歩(33) (0:5/0:0:5)\r
345 | 3 7五歩(76) (0:2/0:0:4)\r
346 | 4 4四歩(43) (0:8/0:0:13)\r
347 | 5 7八飛(28) (0:2/0:0:6)\r
348 | 6 4二飛(82) (0:1/0:0:14)\r
349 | 7 6八銀(79) (0:2/0:0:8)\r
350 | 8 8二銀(71) (0:2/0:0:16)\r
351 | 9 7四歩(75) (0:1/0:0:9)\r
352 | 10 同 歩(73) (0:1/0:0:17)\r
353 | 11 同 飛(78) (0:1/0:0:10)\r
354 | 12 投了 (0:5/0:0:22)\r
355 | """
356 |
357 |
358 | TEST_KIF_CUSTOM_BOARD = """# ---- Kifu for Windows V4.01β 棋譜ファイル ----
359 | # ファイル名:D:\\b\\temp\\M2TOK141\\KIFU\\1t120600-1.kif
360 | 棋戦:1手詰
361 | 戦型:なし
362 | 手合割:平手
363 | 後手の持駒:飛 角 金四 銀三 桂四 香三 歩十七
364 | 9 8 7 6 5 4 3 2 1
365 | +---------------------------+
366 | | ・ ・ ・ ・ ・ ・ ・ ・v香|一
367 | | ・ ・ ・ ・ 飛 馬 ・ ・v玉|二
368 | | ・ ・ ・ ・ ・ ・ ・v歩 ・|三
369 | | ・ ・ ・ ・ ・ ・v銀 ・ ・|四
370 | | ・ ・ ・ ・ ・ ・ ・ ・ ・|五
371 | | ・ ・ ・ ・ ・ ・ ・ ・ ・|六
372 | | ・ ・ ・ ・ ・ ・ ・ ・ ・|七
373 | | ・ ・ ・ ・ ・ ・ ・ ・ ・|八
374 | | ・ ・ ・ ・ ・ ・ ・ ・ ・|九
375 | +---------------------------+
376 | 先手の持駒:なし
377 | 先手:大内延介
378 | 後手:最新詰将棋200選
379 | 手数----指手---------消費時間--
380 | *作者:大内延介
381 | *発表誌:最新詰将棋200選
382 | 1 3一馬(42) ( 0:00/00:00:00)
383 | 2 中断 ( 0:00/00:00:00)
384 | まで1手で中断
385 | """
386 |
387 | TEST_KIF_CUSTOM_BOARD_SHOGI_GUI = """#KIF version=2.0 encoding=UTF-8
388 | 開始日時:2024/02/06 17:26:03
389 | 後手の持駒:飛 角 金四 銀三 桂四 香三 歩十七
390 | 9 8 7 6 5 4 3 2 1
391 | +---------------------------+
392 | | ・ ・ ・ ・ ・ ・ ・ ・v香|一
393 | | ・ ・ ・ ・ 飛 馬 ・ ・v玉|二
394 | | ・ ・ ・ ・ ・ ・ ・v歩 ・|三
395 | | ・ ・ ・ ・ ・ ・v銀 ・ ・|四
396 | | ・ ・ ・ ・ ・ ・ ・ ・ ・|五
397 | | ・ ・ ・ ・ ・ ・ ・ ・ ・|六
398 | | ・ ・ ・ ・ ・ ・ ・ ・ ・|七
399 | | ・ ・ ・ ・ ・ ・ ・ ・ ・|八
400 | | ・ ・ ・ ・ ・ ・ ・ ・ ・|九
401 | +---------------------------+
402 | 先手の持駒:なし
403 | 先手:大内延介
404 | 後手:最新詰将棋200選
405 | 手数----指手---------消費時間--
406 | 1 3一馬(42) ( 0:00/00:00:00)
407 | 2 中断 ( 0:00/00:00:00)
408 | まで1手で中断
409 | """
410 |
411 |
412 | TEST_KIF_EXPORTED_TO_KIF = """開始日時: \r
413 | 終了日時: \r
414 | 手合割:平手\r
415 | 先手:羽生善治\r
416 | 後手:藤井猛\r
417 | 手数----指手---------消費時間-- \r
418 | 1 7六歩(77) \r
419 | 2 3四歩(33) \r
420 | 3 2六歩(27) \r
421 | 4 4四歩(43) \r
422 | 5 4八銀(39) \r
423 | 6 4二飛(82) \r
424 | 7 6八玉(59) \r
425 | 8 6二玉(51) \r
426 | 9 7八玉(68) \r
427 | 10 7二玉(62) \r
428 | 11 5六歩(57) \r
429 | 12 3二銀(31) \r
430 | 13 5七銀(48) \r
431 | 14 4三銀(32) \r
432 | 15 7七角(88) \r
433 | 16 8二玉(72) \r
434 | 17 2五歩(26) \r
435 | 18 3三角(22) \r
436 | 19 8八玉(78) \r
437 | 20 5四銀(43) \r
438 | 21 6六歩(67) \r
439 | 22 9二香(91) \r
440 | 23 9八香(99) \r
441 | 24 9一玉(82) \r
442 | 25 9九玉(88) \r
443 | 26 8二銀(71) \r
444 | 27 8八銀(79) \r
445 | 28 7一金(61) \r
446 | 29 5八金(49) \r
447 | 30 7四歩(73) \r
448 | 31 6八金(58) \r
449 | 32 5二金(41) \r
450 | 33 9六歩(97) \r
451 | 34 9四歩(93) \r
452 | 35 7九金(69) \r
453 | 36 6二金(52) \r
454 | 37 7八金(68) \r
455 | 38 1四歩(13) \r
456 | 39 1六歩(17) \r
457 | 40 6四歩(63) \r
458 | 41 2六飛(28) \r
459 | 42 4五歩(44) \r
460 | 43 3六飛(26) \r
461 | 44 4三銀(54) \r
462 | 45 6八銀(57) \r
463 | 46 7二金(62) \r
464 | 47 8六歩(87) \r
465 | 48 5四歩(53) \r
466 | 49 6五歩(66) \r
467 | 50 6五歩(64) \r
468 | 51 3三角成(77) \r
469 | 52 3三桂(21) \r
470 | 53 3一角打 \r
471 | 54 4一飛(42) \r
472 | 55 6四角成(31) \r
473 | 56 7三銀(82) \r
474 | 57 6五馬(64) \r
475 | 58 6一飛(41) \r
476 | 59 6四歩打 \r
477 | 60 3九角打 \r
478 | 61 5五歩(56) \r
479 | 62 6四銀(73) \r
480 | 63 7四馬(65) \r
481 | 64 7三金(72) \r
482 | 65 7三馬(74) \r
483 | 66 7三銀(64) \r
484 | 67 5四歩(55) \r
485 | 68 5二歩打 \r
486 | 69 6七歩打 \r
487 | 70 6三飛(61) \r
488 | 71 1七桂(29) \r
489 | 72 9三角成(39) \r
490 | 73 5六飛(36) \r
491 | 74 4四銀(43) \r
492 | 75 2四歩(25) \r
493 | 76 2四歩(23) \r
494 | 77 2六飛(56) \r
495 | 78 3五銀(44) \r
496 | 79 5六飛(26) \r
497 | 80 7四角打 \r
498 | 81 5三歩成(54) \r
499 | 82 5三歩(52) \r
500 | 83 6六飛(56) \r
501 | 84 6四歩打 \r
502 | 85 7五歩(76) \r
503 | 86 4七角成(74) \r
504 | 87 7七銀(68) \r
505 | 88 7四歩打 \r
506 | 89 8五歩(86) \r
507 | 90 7五歩(74) \r
508 | 91 5二歩打 \r
509 | 92 6一金(71) \r
510 | 93 8七銀(88) \r
511 | 94 5二金(61) \r
512 | 95 8八金(79) \r
513 | 96 6二金(52) \r
514 | 97 2三金打 \r
515 | 98 5四歩(53) \r
516 | 99 3六歩(37) \r
517 | 100 4四銀(35) \r
518 | 101 2四金(23) \r
519 | 102 7四銀(73) \r
520 | 103 9五歩(96) \r
521 | 104 9五歩(94) \r
522 | 105 3五歩(36) \r
523 | 106 4六歩(45) \r
524 | 107 3四歩(35) \r
525 | 108 4五桂(33) \r
526 | 109 3三歩成(34) \r
527 | 110 8四歩(83) \r
528 | 111 4八歩打 \r
529 | 112 3六馬(47) \r
530 | 113 8六銀(77) \r
531 | 114 8二馬(93) \r
532 | 115 8四歩(85) \r
533 | 116 8五歩打 \r
534 | 117 9五銀(86) \r
535 | 118 9五香(92) \r
536 | 119 9五香(98) \r
537 | 120 9四歩打 \r
538 | 121 9四香(95) \r
539 | 122 9三歩打 \r
540 | 123 8三香打 \r
541 | 124 8三銀(74) \r
542 | 125 8三歩成(84) \r
543 | 126 8三馬(82) \r
544 | 127 9三香成(94) \r
545 | 128 9三桂(81) \r
546 | 129 9四歩打 \r
547 | 130 9五香打 \r
548 | 131 9六銀(87) \r
549 | 132 9八歩打 \r
550 | 133 9八玉(99) \r
551 | 134 9六香(95) \r
552 | 135 9六飛(66) \r
553 | 136 9五歩打 \r
554 | 137 9五飛(96) \r
555 | 138 9二歩打 \r
556 | 139 9三歩成(94) \r
557 | 140 9三歩(92) \r
558 | 141 8五飛(95) \r
559 | 142 8四香打 \r
560 | 143 8四飛(85) \r
561 | 144 8四馬(83) \r
562 | 145 8七香打 \r
563 | 146 8五歩打 \r
564 | 147 9六桂打 \r
565 | 148 7四馬(84) \r
566 | 149 8四香打 \r
567 | 150 7二金(62) \r
568 | 151 9二歩打 \r
569 | 152 9二馬(74) \r
570 | 153 8三歩打 \r
571 | 154 9五銀打 \r
572 | 155 8二銀打 \r
573 | 156 8二金(72) \r
574 | 157 8二歩成(83) \r
575 | 158 8二馬(92) \r
576 | 159 8二香成(84) \r
577 | 160 8二玉(91) \r
578 | 161 7四金打 \r
579 | 162 7二銀打 \r
580 | 163 8三歩打 \r
581 | 164 8三飛(63) \r
582 | 165 8四歩打 \r
583 | 166 5三飛(83) \r
584 | 167 7三歩打 \r
585 | 168 7三飛(53) \r
586 | 169 9一角打 \r
587 | 170 9一玉(82) \r
588 | 171 7三金(74) \r
589 | 172 7三銀(72) \r
590 | 173 8三歩成(84) \r
591 | 174 8一金打 \r
592 | 175 9二歩打 \r
593 | 176 9二金(81) \r
594 | 177 7一飛打 \r
595 | 178 8一香打 \r
596 | 179 9二と(83) \r
597 | 180 9二玉(91) \r
598 | 181 7二飛成(71) \r
599 | 182 投了 \r
600 | まで181手で先手の勝ち\r
601 | """
602 |
603 | TEST_KIF_RESULT = {
604 | "moves": [
605 | "7g7f",
606 | "3c3d",
607 | "2g2f",
608 | "4c4d",
609 | "3i4h",
610 | "8b4b",
611 | "5i6h",
612 | "5a6b",
613 | "6h7h",
614 | "6b7b",
615 | "5g5f",
616 | "3a3b",
617 | "4h5g",
618 | "3b4c",
619 | "8h7g",
620 | "7b8b",
621 | "2f2e",
622 | "2b3c",
623 | "7h8h",
624 | "4c5d",
625 | "6g6f",
626 | "9a9b",
627 | "9i9h",
628 | "8b9a",
629 | "8h9i",
630 | "7a8b",
631 | "7i8h",
632 | "6a7a",
633 | "4i5h",
634 | "7c7d",
635 | "5h6h",
636 | "4a5b",
637 | "9g9f",
638 | "9c9d",
639 | "6i7i",
640 | "5b6b",
641 | "6h7h",
642 | "1c1d",
643 | "1g1f",
644 | "6c6d",
645 | "2h2f",
646 | "4d4e",
647 | "2f3f",
648 | "5d4c",
649 | "5g6h",
650 | "6b7b",
651 | "8g8f",
652 | "5c5d",
653 | "6f6e",
654 | "6d6e",
655 | "7g3c+",
656 | "2a3c",
657 | "B*3a",
658 | "4b4a",
659 | "3a6d+",
660 | "8b7c",
661 | "6d6e",
662 | "4a6a",
663 | "P*6d",
664 | "B*3i",
665 | "5f5e",
666 | "7c6d",
667 | "6e7d",
668 | "7b7c",
669 | "7d7c",
670 | "6d7c",
671 | "5e5d",
672 | "P*5b",
673 | "P*6g",
674 | "6a6c",
675 | "2i1g",
676 | "3i9c+",
677 | "3f5f",
678 | "4c4d",
679 | "2e2d",
680 | "2c2d",
681 | "5f2f",
682 | "4d3e",
683 | "2f5f",
684 | "B*7d",
685 | "5d5c+",
686 | "5b5c",
687 | "5f6f",
688 | "P*6d",
689 | "7f7e",
690 | "7d4g+",
691 | "6h7g",
692 | "P*7d",
693 | "8f8e",
694 | "7d7e",
695 | "P*5b",
696 | "7a6a",
697 | "8h8g",
698 | "6a5b",
699 | "7i8h",
700 | "5b6b",
701 | "G*2c",
702 | "5c5d",
703 | "3g3f",
704 | "3e4d",
705 | "2c2d",
706 | "7c7d",
707 | "9f9e",
708 | "9d9e",
709 | "3f3e",
710 | "4e4f",
711 | "3e3d",
712 | "3c4e",
713 | "3d3c+",
714 | "8c8d",
715 | "P*4h",
716 | "4g3f",
717 | "7g8f",
718 | "9c8b",
719 | "8e8d",
720 | "P*8e",
721 | "8f9e",
722 | "9b9e",
723 | "9h9e",
724 | "P*9d",
725 | "9e9d",
726 | "P*9c",
727 | "L*8c",
728 | "7d8c",
729 | "8d8c+",
730 | "8b8c",
731 | "9d9c+",
732 | "8a9c",
733 | "P*9d",
734 | "L*9e",
735 | "8g9f",
736 | "P*9h",
737 | "9i9h",
738 | "9e9f",
739 | "6f9f",
740 | "P*9e",
741 | "9f9e",
742 | "P*9b",
743 | "9d9c+",
744 | "9b9c",
745 | "9e8e",
746 | "L*8d",
747 | "8e8d",
748 | "8c8d",
749 | "L*8g",
750 | "P*8e",
751 | "N*9f",
752 | "8d7d",
753 | "L*8d",
754 | "6b7b",
755 | "P*9b",
756 | "7d9b",
757 | "P*8c",
758 | "S*9e",
759 | "S*8b",
760 | "7b8b",
761 | "8c8b+",
762 | "9b8b",
763 | "8d8b+",
764 | "9a8b",
765 | "G*7d",
766 | "S*7b",
767 | "P*8c",
768 | "6c8c",
769 | "P*8d",
770 | "8c5c",
771 | "P*7c",
772 | "5c7c",
773 | "B*9a",
774 | "8b9a",
775 | "7d7c",
776 | "7b7c",
777 | "8d8c+",
778 | "G*8a",
779 | "P*9b",
780 | "8a9b",
781 | "R*7a",
782 | "L*8a",
783 | "8c9b",
784 | "9a9b",
785 | "7a7b+",
786 | ],
787 | "sfen": "lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL b - 1",
788 | "names": ["\u7fbd\u751f\u5584\u6cbb", "\u85e4\u4e95\u731b"],
789 | "win": "b",
790 | }
791 |
792 | TEST_KIF_WITH_TIME_RESULT = {
793 | "moves": [
794 | "7g7f",
795 | "3c3d",
796 | "2g2f",
797 | "8c8d",
798 | "2f2e",
799 | "8d8e",
800 | "6i7h",
801 | "4a3b",
802 | "2e2d",
803 | "2c2d",
804 | "2h2d",
805 | "8e8f",
806 | "8g8f",
807 | "8b8f",
808 | "2d3d",
809 | "2b3c",
810 | "3d3f",
811 | "8f8d",
812 | "3f2f",
813 | "3a2b",
814 | "P*8g",
815 | "5a5b",
816 | "3i4h",
817 | "1c1d",
818 | "1g1f",
819 | "2b2c",
820 | "5i5h",
821 | "6a5a",
822 | "4i3h",
823 | "7a6b",
824 | "8h7g",
825 | "9c9d",
826 | "9g9f",
827 | "3c7g+",
828 | "8i7g",
829 | "2a3c",
830 | "7i6h",
831 | "8a9c",
832 | "7f7e",
833 | "P*2d",
834 | "7e7d",
835 | "7c7d",
836 | "P*7b",
837 | "5a6a",
838 | "2f8f",
839 | "8d8f",
840 | "8g8f",
841 | "R*8i",
842 | "7h7i",
843 | "8i9i+",
844 | "R*8a",
845 | "L*7e",
846 | "B*4a",
847 | "5b5a",
848 | "4h5i",
849 | "7e7g+",
850 | "6h7g",
851 | "9i7i",
852 | "7g6h",
853 | "7i9i",
854 | "L*5b",
855 | "5a4b",
856 | "8a6a+",
857 | "N*7f",
858 | "G*7i",
859 | "7f6h+",
860 | "7i6h",
861 | "3c4e",
862 | "6a6b",
863 | "B*8d",
864 | "6b5a",
865 | "8d5a",
866 | "5b5a+",
867 | "4e5g+",
868 | "6h5g",
869 | "9i5i",
870 | "5h5i",
871 | "R*7i",
872 | ],
873 | "sfen": "lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL b - 1",
874 | "names": ["\u884c\u65b9\u5c1a\u53f2", "\u7fbd\u751f\u5584\u6cbb"],
875 | "win": "w",
876 | }
877 |
878 | TEST_KIF_81DOJO_RESULT = {
879 | "moves": ["7g7f", "3c3d", "7f7e", "4c4d", "2h7h", "8b4b", "7i6h", "7a8b", "7e7d", "7c7d", "7h7d"],
880 | "sfen": "lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL b - 1",
881 | "names": ["KikiNoOmata", "XiaoNoOmata"],
882 | "win": "b",
883 | }
884 |
885 | TEST_KIF_CUSTOM_BOARD_RESULT = {
886 | "names": ["大内延介", "最新詰将棋200選"],
887 | "sfen": "8l/4R+B2k/7p1/6s2/9/9/9/9/9 w 1r1b4g3s4n3l17p 1",
888 | "moves": ["4b3a"],
889 | "win": "-",
890 | }
891 |
892 |
893 | class ParserTest(unittest.TestCase):
894 | def test_parse_str(self):
895 | result = KIF.Parser.parse_str(TEST_KIF_STR)
896 | self.assertEqual(result[0], TEST_KIF_RESULT)
897 |
898 | def test_parse_str_with_time(self):
899 | result = KIF.Parser.parse_str(TEST_KIF_STR_WITH_TIME)
900 | self.assertEqual(result[0], TEST_KIF_WITH_TIME_RESULT)
901 |
902 | def test_parse_str_81dojo(self):
903 | result = KIF.Parser.parse_str(TEST_KIF_81DOJO)
904 | self.assertEqual(result[0], TEST_KIF_81DOJO_RESULT)
905 |
906 | def test_parse_file(self):
907 | try:
908 | tempdir = tempfile.mkdtemp()
909 |
910 | # cp932
911 | path = os.path.join(tempdir, "test1.kif")
912 | with codecs.open(path, "w", "cp932") as f:
913 | f.write(TEST_KIF_STR)
914 | result = KIF.Parser.parse_file(path)
915 | self.assertEqual(result[0], TEST_KIF_RESULT)
916 |
917 | # utf-8
918 | path = os.path.join(tempdir, "test2.kif")
919 | with codecs.open(path, "w", "utf-8") as f:
920 | f.write(TEST_KIF_STR)
921 | result = KIF.Parser.parse_file(path)
922 | self.assertEqual(result[0], TEST_KIF_RESULT)
923 |
924 | # utf-8 (BOM)
925 | path = os.path.join(tempdir, "test3.kif")
926 | with codecs.open(path, "w", "utf-8-sig") as f:
927 | f.write(TEST_KIF_STR)
928 | result = KIF.Parser.parse_file(path)
929 | self.assertEqual(result[0], TEST_KIF_RESULT)
930 |
931 | # .kif with custom starting position
932 | for test_str in [TEST_KIF_CUSTOM_BOARD, TEST_KIF_CUSTOM_BOARD_SHOGI_GUI]:
933 | path = os.path.join(tempdir, "test_tsume.kif")
934 | with codecs.open(path, "w", "cp932") as f:
935 | f.write(test_str)
936 | result = KIF.Parser.parse_file(path)
937 | self.assertEqual(result[0], TEST_KIF_CUSTOM_BOARD_RESULT)
938 |
939 | finally:
940 | shutil.rmtree(tempdir)
941 |
942 |
943 | class ExporterTest(unittest.TestCase):
944 | def test_parse_str(self):
945 | result = KIF.Parser.parse_str(TEST_KIF_EXPORTED_TO_KIF)
946 | self.assertEqual(result[0], TEST_KIF_RESULT)
947 |
948 | def test_export_to_kif(self):
949 | result = KIF.Exporter.kif(TEST_KIF_RESULT)
950 | self.assertEqual(result, TEST_KIF_EXPORTED_TO_KIF)
951 |
952 | def test_issue_61(self):
953 | with self.assertRaises(KIF.ExporterException):
954 | # Valid win must be WHITE
955 | sfen_summary = {
956 | "moves": ["7g7f", "3c3d"],
957 | "sfen": "lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL b - 1",
958 | "names": ["羽生善治", "藤井猛"],
959 | "win": "b",
960 | }
961 | KIF.Exporter.kif(sfen_summary)
962 |
--------------------------------------------------------------------------------
/tests/move_test.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # This file is part of the python-shogi library.
4 | # Copyright (C) 2015- Tasuku SUENAGA
5 | #
6 | # This program is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, either version 3 of the License, or
9 | # (at your option) any later version.
10 | #
11 | # This program is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | # GNU General Public License for more details.
15 | #
16 | # You should have received a copy of the GNU General Public License
17 | # along with this program. If not, see .
18 |
19 | import unittest
20 |
21 | import shogi
22 |
23 |
24 | class MoveTestCase(unittest.TestCase):
25 | def test_issue_8(self):
26 | move = shogi.Move.from_usi("9a9b")
27 | self.assertEqual(move.__hash__(), 9)
28 |
29 |
30 | if __name__ == "__main__":
31 | unittest.main()
32 |
--------------------------------------------------------------------------------
/tests/perft_test.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # This file is part of the python-shogi library.
4 | # Copyright (C) 2015- Tasuku SUENAGA
5 | #
6 | # This program is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, either version 3 of the License, or
9 | # (at your option) any later version.
10 | #
11 | # This program is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | # GNU General Public License for more details.
15 | #
16 | # You should have received a copy of the GNU General Public License
17 | # along with this program. If not, see .
18 |
19 | import unittest
20 |
21 | import shogi
22 |
23 |
24 | def perft(board, depth):
25 | if depth > 1:
26 | count = 0
27 |
28 | for move in board.pseudo_legal_moves:
29 | board.push(move)
30 |
31 | if not board.was_suicide() and not board.was_check_by_dropping_pawn(move):
32 | count += perft(board, depth - 1)
33 |
34 | board.pop()
35 |
36 | return count
37 | else:
38 | return len(board.legal_moves)
39 |
40 |
41 | class PerftTestCase(unittest.TestCase):
42 | def test_1(self):
43 | board = shogi.Board()
44 | self.assertEqual(perft(board, 1), 30)
45 | self.assertEqual(perft(board, 2), 900)
46 |
47 | def test_2(self):
48 | board = shogi.Board(shogi.STARTING_SFEN)
49 | self.assertEqual(perft(board, 1), 30)
50 | self.assertEqual(perft(board, 2), 900)
51 |
52 | def test_3(self):
53 | # stalemate
54 | board = shogi.Board("+R+N+SGKG+S+N+R/+B+N+SG+LG+S+N+B/P+LPP+LPP+LP/1P2P2P1/9/9/9/9/4k4 b 9P 200")
55 | self.assertEqual(perft(board, 1), 0)
56 |
57 | def test_4(self):
58 | # max perft with depth 1
59 | board = shogi.Board("R8/2K1S1SSk/4B4/9/9/9/9/9/1L1L1L3 b RBGSNLP3g3n17p 1")
60 | self.assertEqual(perft(board, 1), 593)
61 |
62 | def test_5(self):
63 | board = shogi.Board("4k4/9/9/9/9/9/9/9/9 b 16P 1")
64 | # 81 - (1 king) - (8 cannot move)
65 | self.assertEqual(perft(board, 1), 72)
66 | # 72 * 5 - (5 suicide move) = 355
67 | self.assertEqual(perft(board, 2), 355)
68 |
69 | def test_6(self):
70 | board = shogi.Board("r7k/6K2/7SP/4s2bb/9/9/9/9/9 b r4g2s4n4l17p 1")
71 | self.assertEqual(perft(board, 1), 4)
72 |
73 | def test_7(self):
74 | board = shogi.Board("l7l/5bS2/p1np5/6Sk1/4p2B1/PSpPPn1G1/1P1G2g1N/2+l6/L1KN1+r3 b R3Pgs7p 1")
75 | self.assertEqual(perft(board, 1), 1)
76 |
77 |
78 | if __name__ == "__main__":
79 | unittest.main()
80 |
--------------------------------------------------------------------------------
/tests/person_test.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # This file is part of the python-shogi library.
4 | # Copyright (C) 2015- Tasuku SUENAGA
5 | #
6 | # This program is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, either version 3 of the License, or
9 | # (at your option) any later version.
10 | #
11 | # This program is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | # GNU General Public License for more details.
15 | #
16 | # You should have received a copy of the GNU General Public License
17 | # along with this program. If not, see .
18 |
19 | import unittest
20 |
21 | from shogi import Person
22 |
23 |
24 | class NameTestCase(unittest.TestCase):
25 | def test_is_professional(self):
26 | result = Person.Name.is_professional("羽生 善治 名人・棋聖・王位・王座")
27 | self.assertTrue(result)
28 |
--------------------------------------------------------------------------------