├── .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 | --------------------------------------------------------------------------------