├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .scalafmt.conf ├── LICENSE ├── README.md ├── build.sbt ├── export-all.sh ├── export-broadcast.sh ├── export-broadcasts.sh ├── export-eval.sh ├── export-puzzle.sh ├── export-single-variant.sh ├── export.sh ├── out └── .gitignore ├── project ├── build.properties └── plugins.sbt ├── src └── main │ ├── resources │ ├── application.conf │ └── logback.xml │ └── scala │ ├── Evals.scala │ ├── GWC.scala │ ├── Games.scala │ ├── LightUser.scala │ ├── Puzzles.scala │ ├── Reporter.scala │ ├── db.scala │ ├── db │ ├── BSON.scala │ ├── Handlers.scala │ └── dsl.scala │ └── lila │ ├── ByteArray.scala │ ├── CorrespondenceClock.scala │ ├── Eval.scala │ ├── PerfType.scala │ ├── Sequence.scala │ ├── analyse │ ├── Advice.scala │ ├── Analysis.scala │ ├── Annotator.scala │ └── Info.scala │ ├── game │ ├── BSONHandlers.scala │ ├── BinaryFormat.scala │ ├── Game.scala │ ├── LightGame.scala │ ├── Metadata.scala │ ├── PerfPicker.scala │ ├── PgnDump.scala │ ├── PgnStorage.scala │ ├── Player.scala │ ├── Pov.scala │ └── Source.scala │ └── package.scala └── web ├── .gitignore ├── broadcast-table.html.tpl ├── chess-social-networks-paper.pdf ├── index.html.tpl ├── index.js ├── package-lock.json ├── package.json ├── style.css ├── table.html.tpl └── yarn.lock /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | pull_request: 5 | branches: ['**'] 6 | push: 7 | branches: ['**'] 8 | 9 | jobs: 10 | openjdk21: 11 | runs-on: ubuntu-latest 12 | steps: 13 | 14 | - name: Checkout current branch 15 | uses: actions/checkout@v4 16 | 17 | - name: Setup sbt 18 | uses: sbt/setup-sbt@v1 19 | 20 | - name: Setup JVM 21 | uses: actions/setup-java@v4 22 | with: 23 | distribution: temurin 24 | java-version: 21 25 | cache: sbt 26 | 27 | - name: Build project 28 | run: sbt compile 29 | 30 | - name: Test project 31 | run: sbt test 32 | 33 | - name: Check Formatting 34 | run: sbt scalafmtCheckAll 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | out/ 3 | project/project 4 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = "3.9.4" 2 | runner.dialect = scala3 3 | 4 | 5 | align.preset = more 6 | maxColumn = 110 7 | spaces.inImportCurlyBraces = true 8 | rewrite.rules = [SortModifiers, AvoidInfix] 9 | rewrite.redundantBraces.stringInterpolation = true 10 | 11 | rewrite.scala3.convertToNewSyntax = yes 12 | rewrite.scala3.removeOptionalBraces = yes 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 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 Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published by 637 | the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lichess DB 2 | 3 | This code exports lichess game database in a standard PGN format. 4 | 5 | Files are available on [https://database.lichess.org](https://database.lichess.org). 6 | 7 | Use them to do great things. Please share the results! 8 | 9 | ## Usage 10 | 11 | ``` 12 | # export Jan 2016 Standard games to lichess_db_2016-01.pgn 13 | sbt "runMain lichess.Games 2016-01" 14 | 15 | # export Jan 2016 Standard games to custom_file.pgn 16 | sbt "runMain lichess.Games 2016-01 custom_file.pgn" 17 | 18 | # export Jan 2016 Atomic games to custom_file.pgn 19 | sbt "runMain lichess.Games 2016-01 custom_file.pgn atomic" 20 | ``` 21 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | inThisBuild( 2 | Seq( 3 | scalaVersion := "3.7.0", 4 | versionScheme := Some("early-semver"), 5 | version := "2.0", 6 | run / javaOptions += "-Dconfig.override_with_env_vars=true" 7 | ) 8 | ) 9 | lazy val app = project 10 | .in(file(".")) 11 | .settings( 12 | name := "lichess-db", 13 | organization := "org.lichess", 14 | scalacOptions -= "-Xfatal-warnings", 15 | scalacOptions ++= Seq( 16 | "-source:future", 17 | "-rewrite", 18 | "-new-syntax", 19 | "-explain", 20 | "-Wunused:all", 21 | "-release:21" 22 | ), 23 | resolvers ++= Seq("lila-maven".at("https://raw.githubusercontent.com/lichess-org/lila-maven/master")), 24 | libraryDependencies ++= Seq( 25 | "org.reactivemongo" %% "reactivemongo" % "1.1.0-RC15", 26 | "org.reactivemongo" %% "reactivemongo-akkastream" % "1.1.0-RC15", 27 | "org.lichess" %% "scalalib-core" % "11.7.0", 28 | "org.lichess" %% "scalachess" % "17.5.0", 29 | "com.typesafe.akka" %% "akka-actor" % "2.6.21", 30 | "com.typesafe.akka" %% "akka-stream" % "2.6.21", 31 | "com.typesafe.akka" %% "akka-slf4j" % "2.6.21", 32 | "org.playframework" %% "play-json" % "3.0.4", 33 | "org.lichess" %% "compression" % "3.0", 34 | "org.slf4j" % "slf4j-nop" % "1.7.36" 35 | // "ch.qos.logback" % "logback-classic" % "1.4.14" 36 | ) 37 | ) 38 | -------------------------------------------------------------------------------- /export-all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | dir=${1} 4 | 5 | export_month() { 6 | echo "---------------------------" 7 | ./export-single-variant.sh "$1-$2" $dir/$3 $3 8 | } 9 | 10 | echo "Export all to $dir" 11 | 12 | variants="standard chess960 antichess atomic crazyhouse horde kingOfTheHill racingKings threeCheck" 13 | 14 | for variant in $variants; do 15 | mkdir -p $dir/$variant 16 | done 17 | 18 | for year in 2013; do 19 | for month in 08 09 10 11 12; do 20 | for variant in $variants; do 21 | export_month $year $month $variant 22 | done 23 | done 24 | done 25 | 26 | for year in 2014 2015 2016; do 27 | for month in 01 02 03 04 05 06 07 08 09 10 11 12; do 28 | for variant in $variants; do 29 | export_month $year $month $variant 30 | done 31 | done 32 | done 33 | 34 | for year in 2017; do 35 | for month in 01 02 03 04 05 06 07 08 09 10; do 36 | for variant in $variants; do 37 | export_month $year $month $variant 38 | done 39 | done 40 | done 41 | -------------------------------------------------------------------------------- /export-broadcast.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | month=${1} 4 | dir=${2}/broadcast 5 | file="lichess_db_broadcast_$month.pgn" 6 | compressed_file="$file.zst" 7 | 8 | # if EXPORT_BROADCAST_TOKEN is not set, exit 9 | if [ -z "$BROADCAST_TOKEN" ]; then 10 | echo "BROADCAST_TOKEN is not set. Exiting." 11 | exit 1 12 | fi 13 | 14 | echo "Export broadcasts of $month to $file" 15 | 16 | cd "$dir" 17 | 18 | url="https://lichess.org/api/broadcast/round/_${month//-/_}.pgn" 19 | 20 | echo "curl $url" 21 | 22 | curl -s $url -H "Authorization: Bearer $BROADCAST_TOKEN" >$file 23 | 24 | echo "Counting games in $file" 25 | 26 | touch counts.txt 27 | grep -v -F "$file" counts.txt >counts.txt.new || touch counts.txt.new 28 | games=$(grep --count -F '[GameURL ' "$file") 29 | echo "$compressed_file $games" >>counts.txt.new 30 | mv counts.txt.new counts.txt 31 | 32 | echo "Compressing $games games to $compressed_file" 33 | 34 | rm -f $compressed_file 35 | nice -n19 pzstd -p10 -19 --verbose $file 36 | 37 | rm $file 38 | 39 | echo "Check summing $compressed_file" 40 | touch sha256sums.txt 41 | grep -v -F "$compressed_file" sha256sums.txt >sha256sums.txt.new || touch sha256sums.txt.new 42 | sha256sum "$compressed_file" | tee --append sha256sums.txt.new 43 | mv sha256sums.txt.new sha256sums.txt 44 | 45 | echo "Done!" 46 | -------------------------------------------------------------------------------- /export-broadcasts.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | dir=$1 4 | 5 | # iterate on all months of the year 6 | for year in $(seq -w 2024 2024); do 7 | for month in $(seq -w 1 12); do 8 | # call export-broadcast.sh with the month and dir 9 | ./export-broadcast.sh "$year-$month" $dir 10 | done 11 | done 12 | -------------------------------------------------------------------------------- /export-eval.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | dir=${1} 4 | file="lichess_db_eval.jsonl" 5 | compressed_file="$file.zst" 6 | 7 | echo "Export evals to $dir/$file" 8 | 9 | nice -n19 sbt "runMain lichess.Evals $dir/$file" 10 | 11 | cd "$dir" 12 | 13 | echo "Counting evals in $file" 14 | 15 | evals=$(wc -l "$file") 16 | echo "$evals" >eval-count.txt 17 | 18 | echo "Compressing $evals evals to $compressed_file" 19 | 20 | rm -f $compressed_file 21 | nice -n19 pzstd -p10 -19 --verbose $file 22 | 23 | echo "Check summing $compressed_file" 24 | touch sha256sums.txt 25 | grep -v -F "$compressed_file" sha256sums.txt >sha256sums.txt.new || touch sha256sums.txt.new 26 | sha256sum "$compressed_file" | tee --append sha256sums.txt.new 27 | mv sha256sums.txt.new sha256sums.txt 28 | 29 | echo "Done!" 30 | -------------------------------------------------------------------------------- /export-puzzle.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | dir=${1} 4 | file="lichess_db_puzzle.csv" 5 | compressed_file="$file.zst" 6 | 7 | echo "Export puzzles to $dir/$file" 8 | 9 | nice -n19 sbt "runMain lichess.Puzzles $dir/$file" 10 | 11 | cd "$dir" 12 | 13 | echo "Counting puzzles in $file" 14 | 15 | puzzles=$(tail -n +2 "$file" | wc -l) 16 | echo "$puzzles" >puzzle-count.txt 17 | 18 | echo "Compressing $puzzles puzzles to $compressed_file" 19 | 20 | rm -f $compressed_file 21 | nice -n19 pzstd -p10 -19 --verbose $file 22 | 23 | echo "Check summing $compressed_file" 24 | touch sha256sums.txt 25 | grep -v -F "$compressed_file" sha256sums.txt >sha256sums.txt.new || touch sha256sums.txt.new 26 | sha256sum "$compressed_file" | tee --append sha256sums.txt.new 27 | mv sha256sums.txt.new sha256sums.txt 28 | 29 | echo "Done!" 30 | -------------------------------------------------------------------------------- /export-single-variant.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | month=${1} 4 | dir=${2} 5 | variant=${3} 6 | file="lichess_db_${variant}_rated_$month.pgn" 7 | compressed_file="$file.zst" 8 | 9 | echo "Export $variant games of $month to $file" 10 | 11 | nice -n19 sbt "runMain lichess.Games $month $dir/$file $variant" 12 | 13 | cd "$dir" 14 | 15 | echo "Counting games in $compressed_file" 16 | 17 | touch counts.txt 18 | grep -v -F "$file" counts.txt >counts.txt.new || touch counts.txt.new 19 | games=$(grep --count -F '[Site ' "$file") 20 | echo "$compressed_file $games" >>counts.txt.new 21 | mv counts.txt.new counts.txt 22 | 23 | echo "Compressing $games games to $compressed_file" 24 | 25 | rm -f $compressed_file 26 | nice -n19 pzstd -p10 -19 --verbose $file 27 | 28 | rm $file 29 | 30 | echo "Check summing $compressed_file" 31 | touch sha256sums.txt 32 | grep -v -F "$compressed_file" sha256sums.txt >sha256sums.txt.new || touch sha256sums.txt.new 33 | sha256sum "$compressed_file" | tee --append sha256sums.txt.new 34 | mv sha256sums.txt.new sha256sums.txt 35 | 36 | echo "Creating torrent for $compressed_file" 37 | mktorrent --web-seed "https://database.lichess.org/$variant/$compressed_file" --piece-length 20 --announce "udp://tracker.torrent.eu.org:451" "$compressed_file" 38 | 39 | echo "Done!" 40 | -------------------------------------------------------------------------------- /export.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | month=${1} 4 | dir=${2} 5 | 6 | gen_site() { 7 | echo "Generating website" 8 | cd web 9 | nodejs index.js $dir 10 | cd .. 11 | } 12 | 13 | export_variant() { 14 | echo "---------------------------" 15 | mkdir -p $dir/$1 16 | ./export-single-variant.sh $month $dir/$1 $1 17 | } 18 | 19 | ./export-broadcast.sh $month $dir 20 | 21 | variants="standard chess960 antichess atomic crazyhouse horde kingOfTheHill racingKings threeCheck" 22 | 23 | for variant in $variants; do 24 | export_variant $variant 25 | gen_site 26 | done 27 | 28 | ./export-puzzle.sh $dir 29 | ./export-eval.sh $dir 30 | gen_site 31 | -------------------------------------------------------------------------------- /out/.gitignore: -------------------------------------------------------------------------------- 1 | *.pgn 2 | *.zst 3 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.10.11 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.2") 2 | -------------------------------------------------------------------------------- /src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | db { 2 | game { 3 | uri = "mongodb://172.16.0.50:27017,172.16.0.51:27017,172.16.0.52:27017" 4 | # uri = "mongodb://localhost:27017" 5 | } 6 | puzzle { 7 | uri = "mongodb://172.16.0.28:27017" 8 | # uri = "mongodb://localhost:27017" 9 | } 10 | eval { 11 | uri = "mongodb://172.16.0.43:27017" 12 | # uri = "mongodb://localhost:27017" 13 | } 14 | } 15 | mongo-async-driver { 16 | akka { 17 | loggers = ["akka.event.slf4j.Slf4jLogger"] 18 | stdout-loglevel = "WARNING" 19 | loglevel = "WARNING" 20 | } 21 | } 22 | akka { 23 | loggers = ["akka.event.slf4j.Slf4jLogger"] 24 | loglevel = "WARNING" 25 | stdout-loglevel = "WARNING" 26 | } 27 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %-5level %logger{12} %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/main/scala/Evals.scala: -------------------------------------------------------------------------------- 1 | package lichess 2 | 3 | import akka.actor.ActorSystem 4 | import akka.stream.* 5 | import akka.stream.scaladsl.* 6 | import java.nio.file.Paths 7 | import com.typesafe.config.ConfigFactory 8 | import lila.db.dsl.* 9 | import reactivemongo.akkastream.cursorProducer 10 | import reactivemongo.api.* 11 | import reactivemongo.api.bson.* 12 | import scala.concurrent.duration.* 13 | import scala.concurrent.ExecutionContext.Implicits.global 14 | import chess.format.{ BinaryFen, Fen, Uci } 15 | import cats.data.NonEmptyList 16 | import cats.syntax.all.* 17 | import scala.util.{ Success, Try } 18 | import scala.concurrent.Future 19 | import play.api.libs.json.* 20 | import akka.util.ByteString 21 | import akka.NotUsed 22 | 23 | object Evals: 24 | 25 | enum Score: 26 | case Cp(c: Int) 27 | case Mate(m: Int) 28 | case class Pv(score: Score, moves: NonEmptyList[Uci]) 29 | case class Eval(pvs: NonEmptyList[Pv], knodes: Int, depth: Int) 30 | case class Id(position: BinaryFen) 31 | case class Entry(_id: Id, evals: List[Eval]): 32 | def fen = Fen.write(_id.position.read).simple 33 | 34 | given BSONReader[Id] = new: 35 | def readTry(bs: BSONValue) = bs match 36 | case v: BSONBinary => Success(Id(BinaryFen(v.byteArray))) 37 | case _ => handlerBadValue(s"Invalid evalcache id $bs") 38 | given BSONReader[NonEmptyList[Pv]] = new: 39 | private def scoreRead(str: String): Option[Score] = 40 | if str.startsWith("#") then str.drop(1).toIntOption.map(Score.Mate.apply) 41 | else str.toIntOption.map(Score.Cp.apply) 42 | private def movesRead(str: String): Option[NonEmptyList[Uci]] = 43 | Uci.readListChars(str).flatMap(_.toNel) 44 | private val scoreSeparator = ':' 45 | private val pvSeparator = '/' 46 | def readTry(bs: BSONValue) = bs match 47 | case BSONString(value) => 48 | Try { 49 | value.split(pvSeparator).toList.map { pvStr => 50 | pvStr.split(scoreSeparator) match 51 | case Array(score, moves) => 52 | Pv( 53 | scoreRead(score).getOrElse(sys.error(s"Invalid score $score")), 54 | movesRead(moves).getOrElse(sys.error(s"Invalid moves $moves")) 55 | ) 56 | case x => sys.error(s"Invalid PV $pvStr: ${x.toList} (in $value)") 57 | } 58 | }.map { 59 | _.toNel.getOrElse(sys.error(s"Empty PVs $value")) 60 | } 61 | case b => lila.db.BSON.handlerBadType[NonEmptyList[Pv]](b) 62 | given BSONDocumentReader[Eval] = Macros.reader 63 | given BSONDocumentReader[Entry] = Macros.reader 64 | 65 | def main(args: Array[String]): Unit = 66 | 67 | val path = args.headOption.getOrElse("out/lichess_db_eval.jsonl") 68 | 69 | println(s"Exporting to $path") 70 | 71 | val config = ConfigFactory.load() 72 | val dbName = "lichess" 73 | val collName = "eval_cache2" 74 | 75 | val uri = config.getString("db.eval.uri") 76 | val driver = new AsyncDriver(Some(config.getConfig("mongo-async-driver"))) 77 | 78 | given system: ActorSystem = ActorSystem() 79 | given Materializer = ActorMaterializer( 80 | ActorMaterializerSettings(system) 81 | .withInputBuffer( 82 | initialSize = 32, 83 | maxSize = 32 84 | ) 85 | ) 86 | 87 | def toJson: Flow[Entry, JsObject, NotUsed] = Flow[Entry].map: entry => 88 | Json.obj( 89 | "fen" -> entry.fen.value, 90 | "evals" -> entry.evals.map: eval => 91 | Json.obj( 92 | "pvs" -> eval.pvs.toList.map: pv => 93 | val base = pv.score match 94 | case Score.Cp(c) => Json.obj("cp" -> c) 95 | case Score.Mate(m) => Json.obj("mate" -> m) 96 | base + ("line" -> JsString(pv.moves.map(_.uci).toList.mkString(" "))) 97 | , 98 | "knodes" -> eval.knodes, 99 | "depth" -> eval.depth 100 | ) 101 | ) 102 | 103 | def ndjsonSink: Sink[JsObject, Future[IOResult]] = 104 | Flow[JsObject] 105 | .map { obj => 106 | ByteString(s"${Json.stringify(obj)}\n") 107 | } 108 | .toMat(FileIO.toPath(Paths.get(path)))(Keep.right) 109 | 110 | val process = MongoConnection 111 | .fromString(uri) 112 | .flatMap { parsedUri => 113 | driver.connect(parsedUri, Some("lichess-eval")) 114 | } 115 | .flatMap(_.database(dbName)) 116 | .flatMap { 117 | _.collection(collName) 118 | .find($doc()) 119 | .cursor[Entry]() 120 | // .cursor[Entry](readPreference = ReadPreference.secondary) 121 | .documentSource( 122 | // maxDocs = 1000, 123 | maxDocs = Int.MaxValue, 124 | err = Cursor.ContOnError((_, e) => println(e.getMessage)) 125 | ) 126 | .buffer(1000, OverflowStrategy.backpressure) 127 | .filter(_._id.position.read.board.variant.standard) 128 | .via(toJson) 129 | .runWith(ndjsonSink) 130 | } 131 | 132 | scala.concurrent.Await.result(process, Duration.Inf) 133 | println("done") 134 | system.terminate() 135 | -------------------------------------------------------------------------------- /src/main/scala/GWC.scala: -------------------------------------------------------------------------------- 1 | package lichess 2 | 3 | import scala.concurrent.duration.* 4 | import scala.concurrent.ExecutionContext.Implicits.global 5 | import scala.concurrent.Future 6 | 7 | import reactivemongo.api.* 8 | import reactivemongo.api.bson.* 9 | 10 | import akka.actor.ActorSystem 11 | import akka.stream.* 12 | import akka.stream.scaladsl.* 13 | import akka.util.ByteString 14 | import java.nio.file.Paths 15 | import reactivemongo.akkastream.cursorProducer 16 | 17 | import lila.analyse.Analysis 18 | import lila.game.{ Game, PgnDump } 19 | import lila.db.dsl.* 20 | import java.time.LocalDateTime 21 | 22 | object GWC: 23 | 24 | def main(args: Array[String]): Unit = 25 | val path = args(0) 26 | 27 | val variant = chess.variant.Standard 28 | 29 | val from = LocalDateTime.parse("2024-07-19T22:00:00.00") 30 | val to = LocalDateTime.parse("2024-07-20T22:00:00.00") 31 | val toInstant = to.toInstant(java.time.ZoneOffset.UTC) 32 | 33 | println(s"Export $from to $path") 34 | 35 | given system: ActorSystem = ActorSystem() 36 | given ActorMaterializer = ActorMaterializer( 37 | ActorMaterializerSettings(system) 38 | .withInputBuffer( 39 | initialSize = 32, 40 | maxSize = 32 41 | ) 42 | ) 43 | 44 | val process = lichess.DB.get.flatMap { (db, close) => 45 | 46 | val query = BSONDocument( 47 | "ca" -> BSONDocument("$gte" -> from, "$lt" -> to), 48 | "ra" -> true, 49 | "v" -> BSONDocument("$exists" -> variant.exotic) 50 | ) 51 | 52 | val gameSource = db.gameColl 53 | .find(query) 54 | .sort(BSONDocument("ca" -> 1)) 55 | .cursor[BSONDocument](readPreference = ReadPreference.primary) 56 | .documentSource( 57 | maxDocs = Int.MaxValue, 58 | err = Cursor.ContOnError((_, e) => println(e.getMessage)) 59 | ) 60 | 61 | val tickSource = 62 | Source.tick(Reporter.freq, Reporter.freq, None) 63 | 64 | // def checkLegality(g: Game): Future[(Game, Boolean)] = Future { 65 | // g -> chess.Replay 66 | // .boards(g.sans, None, g.variant) 67 | // .fold( 68 | // err => { 69 | // println(s"Replay error ${g.id} ${err.toString.take(60)}") 70 | // false 71 | // }, 72 | // boards => { 73 | // if boards.size == g.sans.size + 1 then true 74 | // else { 75 | // println( 76 | // s"Replay error ${g.id} boards.size=${boards.size}, moves.size=${g.sans.size}" 77 | // ) 78 | // false 79 | // } 80 | // } 81 | // ) 82 | // } 83 | 84 | def bsonRead(variant: chess.variant.Variant)(docs: Seq[BSONDocument]) = Future { 85 | docs 86 | .map(lila.game.BSONHandlers.gameWithInitialFenBSONHandler.read) 87 | .filter(_.game.variant == variant) 88 | } 89 | 90 | type Analysed = (Game.WithInitialFen, Option[Analysis]) 91 | def withAnalysis(gs: Seq[Game.WithInitialFen]): Future[Seq[Analysed]] = 92 | Future.successful(gs.map(_ -> None)) 93 | 94 | type WithUsers = (Analysed, Users) 95 | def withUsers(as: Seq[Analysed]): Future[Seq[WithUsers]] = 96 | db.users(as.map(_._1.game)).map { users => 97 | as.zip(users) 98 | } 99 | 100 | def toPgn(ws: Seq[WithUsers]): Future[ByteString] = 101 | Future { 102 | val str = ws 103 | .map { case ((g, analysis), users) => 104 | val pgn = PgnDump(g.game, users, g.fen) 105 | lila.analyse.Annotator(pgn, analysis) 106 | } 107 | .map(_.toString) 108 | .mkString("\n\n") 109 | .replace("] } { [", "] [") 110 | ByteString(s"$str\n\n") 111 | } 112 | 113 | def pgnSink: Sink[ByteString, Future[IOResult]] = 114 | Flow[ByteString].toMat(FileIO.toPath(Paths.get(path)))(Keep.right) 115 | 116 | gameSource 117 | .buffer(10000, OverflowStrategy.backpressure) 118 | .grouped(64) 119 | .mapAsyncUnordered(12)(bsonRead(variant)) 120 | .map(g => Some(g)) 121 | .merge(tickSource, eagerComplete = true) 122 | .via(Reporter.graph) 123 | // .mapAsyncUnordered(16)(checkLegality) 124 | .map(_.filter(_.game.movedAt.isBefore(toInstant))) 125 | // .filter(_._2).map(_._1) 126 | .mapAsyncUnordered(16)(withAnalysis) 127 | .mapAsyncUnordered(16)(withUsers) 128 | .mapAsyncUnordered(12)(toPgn) 129 | .runWith(pgnSink) 130 | .andThen { case _ => close() } 131 | 132 | } 133 | 134 | scala.concurrent.Await.result(process, Duration.Inf) 135 | println("done") 136 | system.terminate() 137 | -------------------------------------------------------------------------------- /src/main/scala/Games.scala: -------------------------------------------------------------------------------- 1 | package lichess 2 | 3 | import scala.concurrent.duration.* 4 | import scala.concurrent.ExecutionContext.Implicits.global 5 | import scala.concurrent.Future 6 | 7 | import reactivemongo.api.* 8 | import reactivemongo.api.bson.* 9 | 10 | import akka.actor.ActorSystem 11 | import akka.stream.* 12 | import akka.stream.scaladsl.* 13 | import akka.util.ByteString 14 | import java.nio.file.Paths 15 | import reactivemongo.akkastream.cursorProducer 16 | 17 | import chess.variant.{ Horde, Variant } 18 | import lila.analyse.Analysis 19 | import lila.analyse.Analysis.analysisBSONHandler 20 | import lila.game.{ Game, PgnDump } 21 | import lila.db.dsl.* 22 | import java.time.LocalDate 23 | 24 | object Games: 25 | 26 | def main(args: Array[String]): Unit = 27 | val fromStr = args.lift(0).getOrElse("2015-01") 28 | 29 | val path = 30 | args.lift(1).getOrElse("out/lichess_db_%.pgn").replace("%", fromStr) 31 | 32 | val variant = Variant 33 | .apply(Variant.LilaKey(args.lift(2).getOrElse("standard"))) 34 | .getOrElse(throw new RuntimeException("Invalid variant.")) 35 | 36 | val fromWithoutAdjustments = LocalDate.parse(s"$fromStr-01").atStartOfDay 37 | val to = fromWithoutAdjustments.plusMonths(1) 38 | 39 | val hordeStartDate = java.time.LocalDateTime.of(2015, 4, 11, 10, 0) 40 | val from = 41 | if variant == Horde && hordeStartDate.isAfter(fromWithoutAdjustments) 42 | then hordeStartDate 43 | else fromWithoutAdjustments 44 | 45 | if !from.isBefore(to) then 46 | System.out.println("Too early for Horde games. Exiting."); 47 | System.exit(0); 48 | 49 | println(s"Export $from to $path") 50 | 51 | given system: ActorSystem = ActorSystem() 52 | given ActorMaterializer = ActorMaterializer( 53 | ActorMaterializerSettings(system) 54 | .withInputBuffer( 55 | initialSize = 32, 56 | maxSize = 32 57 | ) 58 | ) 59 | 60 | val process = lichess.DB.get.flatMap { (db, close) => 61 | 62 | val query = BSONDocument( 63 | "ca" -> BSONDocument("$gte" -> from, "$lt" -> to), 64 | "ra" -> true, 65 | "v" -> BSONDocument("$exists" -> variant.exotic) 66 | ) 67 | 68 | val gameSource = db.gameColl 69 | .find(query) 70 | .sort(BSONDocument("ca" -> 1)) 71 | .cursor[BSONDocument](readPreference = ReadPreference.primary) 72 | .documentSource( 73 | maxDocs = Int.MaxValue, 74 | err = Cursor.ContOnError((_, e) => println(e.getMessage)) 75 | ) 76 | 77 | val tickSource = 78 | Source.tick(Reporter.freq, Reporter.freq, None) 79 | 80 | // def checkLegality(g: Game): Future[(Game, Boolean)] = Future { 81 | // g -> chess.Replay 82 | // .boards(g.sans, None, g.variant) 83 | // .fold( 84 | // err => { 85 | // println(s"Replay error ${g.id} ${err.toString.take(60)}") 86 | // false 87 | // }, 88 | // boards => { 89 | // if boards.size == g.sans.size + 1 then true 90 | // else { 91 | // println( 92 | // s"Replay error ${g.id} boards.size=${boards.size}, moves.size=${g.sans.size}" 93 | // ) 94 | // false 95 | // } 96 | // } 97 | // ) 98 | // } 99 | 100 | def bsonRead(variant: Variant)(docs: Seq[BSONDocument]) = Future { 101 | docs 102 | .map(lila.game.BSONHandlers.gameWithInitialFenBSONHandler.read) 103 | .filter(_.game.variant == variant) 104 | } 105 | 106 | type Analysed = (Game.WithInitialFen, Option[Analysis]) 107 | def withAnalysis(gs: Seq[Game.WithInitialFen]): Future[Seq[Analysed]] = 108 | db.analysisColl 109 | .find( 110 | BSONDocument( 111 | "_id" -> BSONDocument( 112 | "$in" -> gs.filter(_.game.metadata.analysed).map(_.game.id) 113 | ) 114 | ) 115 | ) 116 | .cursor[Analysis](readPreference = ReadPreference.secondaryPreferred) 117 | .collect[List](Int.MaxValue, Cursor.ContOnError[List[Analysis]]()) 118 | .map { as => 119 | gs.map { g => 120 | g -> as.find(_.id == g.game.id) 121 | } 122 | } 123 | 124 | type WithUsers = (Analysed, Users) 125 | def withUsers(as: Seq[Analysed]): Future[Seq[WithUsers]] = 126 | db.users(as.map(_._1.game)).map { users => 127 | as.zip(users) 128 | } 129 | 130 | def toPgn(ws: Seq[WithUsers]): Future[ByteString] = 131 | Future { 132 | ByteString { 133 | ws.map { case ((g, analysis), users) => 134 | val pgn = PgnDump(g.game, users, g.fen) 135 | lila.analyse 136 | .Annotator(pgn, analysis) 137 | .render 138 | .value 139 | .replace("] } { [", "] [") + "\n\n" 140 | }.mkString 141 | } 142 | } 143 | 144 | def pgnSink: Sink[ByteString, Future[IOResult]] = 145 | Flow[ByteString].toMat(FileIO.toPath(Paths.get(path)))(Keep.right) 146 | 147 | gameSource 148 | .buffer(10000, OverflowStrategy.backpressure) 149 | .grouped(64) 150 | .mapAsyncUnordered(12)(bsonRead(variant)) 151 | .map(g => Some(g)) 152 | .merge(tickSource, eagerComplete = true) 153 | .via(Reporter.graph) 154 | // .mapAsyncUnordered(16)(checkLegality) 155 | // .filter(_._2).map(_._1) 156 | .mapAsyncUnordered(16)(withAnalysis) 157 | .mapAsyncUnordered(16)(withUsers) 158 | .mapAsyncUnordered(12)(toPgn) 159 | .runWith(pgnSink) 160 | .andThen { case _ => close() } 161 | 162 | } 163 | 164 | scala.concurrent.Await.result(process, Duration.Inf) 165 | println("done") 166 | system.terminate() 167 | -------------------------------------------------------------------------------- /src/main/scala/LightUser.scala: -------------------------------------------------------------------------------- 1 | package lichess 2 | 3 | case class LightUser(id: String, name: String, title: Option[String] = None) 4 | 5 | case class Users(white: LightUser, black: LightUser): 6 | 7 | def apply(color: chess.Color) = color.fold(white, black) 8 | -------------------------------------------------------------------------------- /src/main/scala/Puzzles.scala: -------------------------------------------------------------------------------- 1 | package lichess 2 | 3 | import akka.actor.ActorSystem 4 | import akka.stream.* 5 | import akka.stream.scaladsl.* 6 | import akka.util.ByteString 7 | import chess.format.{ Fen, FullFen } 8 | import com.typesafe.config.ConfigFactory 9 | import java.nio.file.Paths 10 | import lila.db.dsl.* 11 | import reactivemongo.akkastream.cursorProducer 12 | import reactivemongo.api.* 13 | import reactivemongo.api.bson.* 14 | import scala.concurrent.duration.* 15 | import scala.concurrent.ExecutionContext.Implicits.global 16 | import scala.concurrent.Future 17 | 18 | object Puzzles: 19 | 20 | case class PuzzleLine( 21 | id: String, 22 | fen: Fen.Full, 23 | moves: List[String], 24 | rating: Int, 25 | ratingDev: Int, 26 | popularity: Int, 27 | plays: Int, 28 | themes: List[String], 29 | openings: List[String], 30 | gameUrl: String 31 | ) 32 | 33 | def main(args: Array[String]): Unit = 34 | 35 | val path = args.headOption.getOrElse("out/lichess_db_puzzle.csv") 36 | 37 | println(s"Exporting to $path") 38 | 39 | val config = ConfigFactory.load() 40 | val dbName = "puzzler" 41 | val collName = "puzzle2_puzzle" 42 | 43 | val uri = config.getString("db.puzzle.uri") 44 | val driver = new AsyncDriver(Some(config.getConfig("mongo-async-driver"))) 45 | 46 | given system: ActorSystem = ActorSystem() 47 | given Materializer = ActorMaterializer( 48 | ActorMaterializerSettings(system) 49 | .withInputBuffer( 50 | initialSize = 32, 51 | maxSize = 32 52 | ) 53 | ) 54 | 55 | val hiddenThemes = Set("checkFirst") 56 | 57 | def parseDoc(doc: Bdoc): Option[PuzzleLine] = for 58 | id <- doc.string("_id") 59 | fen <- FullFen.from(doc.string("fen")) 60 | moves <- doc.string("line") 61 | glicko <- doc.child("glicko") 62 | rating <- glicko.double("r") 63 | rd <- glicko.double("d") 64 | popularity <- doc.double("vote") 65 | plays <- doc.int("plays") 66 | themes <- doc.getAsOpt[List[String]]("themes") 67 | gameId <- doc.string("gameId") 68 | openings = doc.getAsOpt[List[String]]("opening") 69 | yield PuzzleLine( 70 | id = id, 71 | fen = fen, 72 | moves = moves.split(' ').toList, 73 | rating = rating.toInt, 74 | ratingDev = rd.toInt, 75 | popularity = math.round(popularity * 100).toInt, 76 | plays = plays, 77 | themes = themes.filterNot(hiddenThemes.contains), 78 | gameUrl = 79 | val asWhite = fen.colorOrWhite.white 80 | val hash = Fen.readPly(fen).map(_ + 1).fold("")(p => s"#$p") 81 | s"https://lichess.org/${gameId}${if asWhite then "" else "/black"}$hash" 82 | , 83 | openings = openings.getOrElse(Nil) 84 | ) 85 | 86 | def toCsvLine(puzzle: PuzzleLine): String = 87 | List( 88 | puzzle.id, 89 | puzzle.fen.value, 90 | puzzle.moves.mkString(" "), 91 | puzzle.rating, 92 | puzzle.ratingDev, 93 | puzzle.popularity, 94 | puzzle.plays, 95 | puzzle.themes.sorted.mkString(" "), 96 | puzzle.gameUrl, 97 | puzzle.openings.mkString(" ") 98 | ).mkString(",") 99 | 100 | def csvSink: Sink[String, Future[IOResult]] = 101 | Flow[String] 102 | .map { line => 103 | ByteString(s"$line\n") 104 | } 105 | .toMat(FileIO.toPath(Paths.get(path)))(Keep.right) 106 | 107 | val process = MongoConnection 108 | .fromString(uri) 109 | .flatMap: parsedUri => 110 | driver.connect(parsedUri, Some("lichess-puzzle")) 111 | .flatMap(_.database(dbName)) 112 | .flatMap { 113 | _.collection(collName) 114 | .find( 115 | BSONDocument( 116 | "issue" -> BSONDocument("$exists" -> false), 117 | "vote" -> BSONDocument("$gt" -> -1), 118 | "$or" -> BSONArray( 119 | BSONDocument("generator" -> BSONDocument("$exists" -> 0)), 120 | BSONDocument("generator" -> BSONDocument("$gt" -> 22)) 121 | ) 122 | ) 123 | ) 124 | .sort(BSONDocument("_id" -> 1)) 125 | .cursor[Bdoc]() 126 | // .cursor[Bdoc](readPreference = ReadPreference.secondary) 127 | .documentSource( 128 | // maxDocs = 10, 129 | maxDocs = Int.MaxValue, 130 | err = Cursor.ContOnError((_, e) => println(e.getMessage)) 131 | ) 132 | .buffer(1000, OverflowStrategy.backpressure) 133 | .mapConcat(d => parseDoc(d).toList) 134 | .map(toCsvLine) 135 | .prepend( 136 | Source( 137 | List("PuzzleId,FEN,Moves,Rating,RatingDeviation,Popularity,NbPlays,Themes,GameUrl,OpeningTags") 138 | ) 139 | ) 140 | .runWith(csvSink) 141 | } 142 | 143 | scala.concurrent.Await.result(process, Duration.Inf) 144 | println("done") 145 | system.terminate() 146 | -------------------------------------------------------------------------------- /src/main/scala/Reporter.scala: -------------------------------------------------------------------------------- 1 | package lichess 2 | 3 | import akka.stream.* 4 | import akka.stream.stage.* 5 | import scala.concurrent.duration.* 6 | import java.time.format.{ DateTimeFormatter, FormatStyle } 7 | import java.time.Instant 8 | import scalalib.time.* 9 | 10 | import lila.game.Game 11 | 12 | object Reporter: 13 | 14 | val freq = 2.seconds 15 | 16 | val graph = new GraphStage[FlowShape[Option[Seq[Game.WithInitialFen]], Seq[Game.WithInitialFen]]]: 17 | 18 | val in = Inlet[Option[Seq[Game.WithInitialFen]]]("reporter.in") 19 | val out = Outlet[Seq[Game.WithInitialFen]]("reporter.out") 20 | override val shape = FlowShape.of(in, out) 21 | 22 | override def createLogic(inheritedAttributes: Attributes): GraphStageLogic = new GraphStageLogic(shape): 23 | 24 | private val formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT) 25 | 26 | private var counter = 0 27 | private var prev = 0 28 | private var date: Option[Instant] = None 29 | 30 | setHandler( 31 | in, 32 | new InHandler: 33 | override def onPush() = 34 | grab(in) match 35 | case Some(gs) => 36 | counter += gs.size 37 | date = gs.headOption.map(_.game.createdAt).orElse(date) 38 | push(out, gs) 39 | case None => 40 | val gps = (counter - prev) / freq.toSeconds 41 | println(s"${date.fold("-")(formatter.print)} $counter $gps/s") 42 | prev = counter 43 | pull(in) 44 | 45 | setHandler( 46 | out, 47 | new OutHandler: 48 | override def onPull() = 49 | pull(in) 50 | ) 51 | 52 | // override def onUpstreamFinish(): Unit = { 53 | // println("finished?") 54 | // completeStage() 55 | // } 56 | ) 57 | -------------------------------------------------------------------------------- /src/main/scala/db.scala: -------------------------------------------------------------------------------- 1 | package lichess 2 | 3 | import com.typesafe.config.ConfigFactory 4 | import reactivemongo.api.* 5 | import reactivemongo.api.bson.* 6 | import reactivemongo.api.bson.collection.BSONCollection 7 | import scala.concurrent.ExecutionContext.Implicits.global 8 | import scala.concurrent.Future 9 | import scala.util.Success 10 | 11 | final class DB( 12 | val gameColl: BSONCollection, 13 | val analysisColl: BSONCollection, 14 | val userColl: BSONCollection 15 | ): 16 | 17 | private val userProj = BSONDocument("username" -> true, "title" -> true) 18 | given lightUserBSONReader: BSONDocumentReader[LightUser] = new: 19 | 20 | def readDocument(doc: BSONDocument) = 21 | Success( 22 | LightUser( 23 | id = doc.string("_id").get, 24 | name = doc.string("username").get, 25 | title = doc.string("title") 26 | ) 27 | ) 28 | 29 | def users(gs: Seq[lila.game.Game]): Future[Seq[Users]] = 30 | userColl 31 | .find( 32 | BSONDocument( 33 | "_id" -> BSONDocument("$in" -> gs.flatMap(_.players.mapList(_.userId).flatten).distinct) 34 | ), 35 | Some(userProj) 36 | ) 37 | .cursor[LightUser](readPreference = ReadPreference.secondary) 38 | .collect[List](Int.MaxValue, Cursor.ContOnError[List[LightUser]]()) 39 | .map { users => 40 | def of(p: lila.game.Player) = p.userId.fold(LightUser("?", "?")) { uid => 41 | users.find(_.id == uid).getOrElse(LightUser(uid, uid)) 42 | } 43 | gs.map { g => 44 | Users(of(g.players.white), of(g.players.black)) 45 | } 46 | } 47 | 48 | object DB: 49 | 50 | private val config = ConfigFactory.load() 51 | 52 | val dbName = "lichess" 53 | val collName = "game5" 54 | 55 | val uri = config.getString("db.game.uri") 56 | val driver = new AsyncDriver(Some(config.getConfig("mongo-async-driver"))) 57 | val conn = 58 | MongoConnection.fromString(uri).flatMap { parsedUri => 59 | driver.connect(parsedUri, Some("lichess-db")) 60 | } 61 | 62 | def get: Future[(DB, () => Unit)] = 63 | conn.flatMap(_.database(dbName)).map { db => 64 | ( 65 | new DB( 66 | gameColl = db.collection("game5"), 67 | analysisColl = db.collection("analysis2"), 68 | userColl = db.collection("user4") 69 | ), 70 | (() => driver.close()) 71 | ) 72 | } 73 | -------------------------------------------------------------------------------- /src/main/scala/db/BSON.scala: -------------------------------------------------------------------------------- 1 | package lila 2 | package db 3 | 4 | import dsl.* 5 | import alleycats.Zero 6 | import reactivemongo.api.bson.* 7 | import scala.util.Try 8 | 9 | abstract class BSON[T] extends BSONReadOnly[T] with BSONDocumentReader[T] with BSONDocumentWriter[T]: 10 | 11 | val logMalformed = true 12 | 13 | def write(obj: T): Bdoc = ??? 14 | 15 | def writeTry(obj: T): Try[Bdoc] = ??? 16 | 17 | abstract class BSONReadOnly[T] extends BSONDocumentReader[T]: 18 | 19 | import BSON.* 20 | 21 | def reads(reader: Reader): T 22 | 23 | def readDocument(doc: Bdoc) = 24 | Try { 25 | reads(new Reader(doc)) 26 | } 27 | 28 | def read(doc: Bdoc) = readDocument(doc).get 29 | 30 | object BSON extends Handlers: 31 | 32 | final class Reader(val doc: Bdoc): 33 | 34 | def get[A: BSONReader](k: String): A = 35 | doc.getAsTry[A](k).get 36 | def getO[A: BSONReader](k: String): Option[A] = 37 | doc.getAsOpt[A](k) 38 | def getD[A](k: String)(implicit zero: Zero[A], reader: BSONReader[A]): A = 39 | doc.getAsOpt[A](k).getOrElse(zero.zero) 40 | def getD[A: BSONReader](k: String, default: => A): A = 41 | doc.getAsOpt[A](k).getOrElse(default) 42 | def getsD[A: BSONReader](k: String) = 43 | doc.getAsOpt[List[A]](k).getOrElse(Nil) 44 | 45 | inline def str(k: String) = get[String](k)(using BSONStringHandler) 46 | inline def strO(k: String) = getO[String](k)(using BSONStringHandler) 47 | def strD(k: String) = strO(k).getOrElse("") 48 | def int(k: String) = get[Int](k) 49 | def intO(k: String) = getO[Int](k) 50 | def intD(k: String) = intO(k).getOrElse(0) 51 | def double(k: String) = get[Double](k) 52 | def doubleO(k: String) = getO[Double](k) 53 | def floatO(k: String) = getO[Float](k) 54 | def bool(k: String) = get[Boolean](k) 55 | def boolO(k: String) = getO[Boolean](k) 56 | def boolD(k: String) = boolO(k).getOrElse(false) 57 | def date(k: String) = get[Instant](k) 58 | def dateO(k: String) = getO[Instant](k) 59 | def dateD(k: String, default: => Instant) = getD(k, default) 60 | def bytes(k: String) = get[ByteArray](k) 61 | def bytesO(k: String) = getO[ByteArray](k) 62 | def bytesD(k: String) = bytesO(k).getOrElse(ByteArray.empty) 63 | def nInt(k: String) = get[BSONNumberLike](k).toInt.get 64 | def nIntO(k: String): Option[Int] = getO[BSONNumberLike](k).flatMap(_.toInt.toOption) 65 | def nIntD(k: String): Int = nIntO(k).getOrElse(0) 66 | def intsD(k: String) = getO[List[Int]](k).getOrElse(Nil) 67 | def strsD(k: String) = getO[List[String]](k).getOrElse(Nil) 68 | 69 | export doc.contains 70 | 71 | def debug = BSON.debug(doc) 72 | 73 | def debug(v: BSONValue): String = v match 74 | case d: Bdoc => debugDoc(d) 75 | case d: Barr => debugArr(d) 76 | case BSONString(x) => x 77 | case BSONInteger(x) => x.toString 78 | case BSONDouble(x) => x.toString 79 | case BSONBoolean(x) => x.toString 80 | case v => v.toString 81 | def debugArr(doc: Barr): String = doc.values.toList.map(debug).mkString("[", ", ", "]") 82 | def debugDoc(doc: Bdoc): String = 83 | (doc.elements.toList 84 | .map { 85 | case BSONElement(k, v) => s"$k: ${debug(v)}" 86 | case x => x.toString 87 | }) 88 | .mkString("{", ", ", "}") 89 | 90 | def hashDoc(doc: Bdoc): String = debugDoc(doc).replace(" ", "") 91 | -------------------------------------------------------------------------------- /src/main/scala/db/Handlers.scala: -------------------------------------------------------------------------------- 1 | package lila 2 | package db 3 | 4 | import reactivemongo.api.bson.* 5 | import scala.util.{ Failure, Success, Try } 6 | import reactivemongo.api.bson.exceptions.TypeDoesNotMatchException 7 | 8 | trait Handlers: 9 | 10 | // free handlers for all types with TotalWrapper 11 | // unless they are given an instance of lila.db.NoDbHandler[T] 12 | given [T, A] => ( 13 | sr: SameRuntime[A, T], 14 | rs: SameRuntime[T, A], 15 | handler: BSONHandler[A] 16 | ) => BSONHandler[T] = 17 | handler.as(sr.apply, rs.apply) 18 | 19 | def quickHandler[T](read: PartialFunction[BSONValue, T], write: T => BSONValue): BSONHandler[T] = 20 | new BSONHandler[T]: 21 | def readTry(bson: BSONValue) = 22 | read 23 | .andThen(Success(_)) 24 | .applyOrElse( 25 | bson, 26 | (b: BSONValue) => handlerBadType(b) 27 | ) 28 | def writeTry(t: T) = Success(write(t)) 29 | 30 | def tryHandler[T](read: PartialFunction[BSONValue, Try[T]], write: T => BSONValue): BSONHandler[T] = 31 | new BSONHandler[T]: 32 | def readTry(bson: BSONValue) = 33 | read.applyOrElse( 34 | bson, 35 | (b: BSONValue) => handlerBadType(b) 36 | ) 37 | def writeTry(t: T) = Success(write(t)) 38 | 39 | def handlerBadType[T](b: BSONValue): Try[T] = 40 | Failure(TypeDoesNotMatchException("BSONValue", b.getClass.getSimpleName)) 41 | 42 | def handlerBadValue[T](msg: String): Try[T] = 43 | Failure(new IllegalArgumentException(msg)) 44 | 45 | def stringMapHandler[V](implicit 46 | reader: BSONReader[Map[String, V]], 47 | writer: BSONWriter[Map[String, V]] 48 | ) = 49 | new BSONHandler[Map[String, V]]: 50 | def readTry(bson: BSONValue) = reader.readTry(bson) 51 | def writeTry(v: Map[String, V]) = writer.writeTry(v) 52 | 53 | given colorBoolHandler: BSONHandler[chess.Color] = 54 | BSONBooleanHandler.as[chess.Color](chess.Color.fromWhite(_), _.white) 55 | -------------------------------------------------------------------------------- /src/main/scala/db/dsl.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2014 Fehmi Can Saglam (@fehmicans) and contributors. 2 | // See the LICENCE.txt file distributed with this work for additional 3 | // information regarding copyright ownership. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package lila.db 18 | 19 | import alleycats.Zero 20 | import reactivemongo.api.* 21 | import reactivemongo.api.bson.* 22 | 23 | trait dsl: 24 | 25 | type Coll = reactivemongo.api.bson.collection.BSONCollection 26 | type Bdoc = BSONDocument 27 | type Barr = BSONArray 28 | 29 | // **********************************************************************************************// 30 | // Helpers 31 | val $empty: Bdoc = document.asStrict 32 | 33 | def $doc(elements: ElementProducer*): Bdoc = BSONDocument.strict(elements*) 34 | 35 | def $doc(elements: Iterable[(String, BSONValue)]): Bdoc = BSONDocument.strict(elements) 36 | 37 | def $arr(elements: Producer[BSONValue]*): Barr = BSONArray(elements*) 38 | 39 | def $id[T: BSONWriter](id: T): Bdoc = $doc("_id" -> id) 40 | 41 | def $inIds[T: BSONWriter](ids: Iterable[T]): Bdoc = 42 | $id($doc("$in" -> ids)) 43 | 44 | def $boolean(b: Boolean) = BSONBoolean(b) 45 | def $string(s: String) = BSONString(s) 46 | def $int(i: Int) = BSONInteger(i) 47 | 48 | // End of Helpers 49 | // **********************************************************************************************// 50 | 51 | implicit val LilaBSONDocumentZero: Zero[Bdoc] = Zero($empty) 52 | 53 | // **********************************************************************************************// 54 | // Top Level Logical Operators 55 | def $or(expressions: Bdoc*): Bdoc = 56 | $doc("$or" -> expressions) 57 | 58 | def $and(expressions: Bdoc*): Bdoc = 59 | $doc("$and" -> expressions) 60 | 61 | def $nor(expressions: Bdoc*): Bdoc = 62 | $doc("$nor" -> expressions) 63 | // End of Top Level Logical Operators 64 | // **********************************************************************************************// 65 | 66 | // **********************************************************************************************// 67 | // Top Level Evaluation Operators 68 | def $text(term: String): Bdoc = 69 | $doc("$text" -> $doc("$search" -> term)) 70 | 71 | def $text(term: String, lang: String): Bdoc = 72 | $doc("$text" -> $doc("$search" -> term, f"$$language" -> lang)) 73 | 74 | def $where(expr: String): Bdoc = 75 | $doc("$where" -> expr) 76 | // End of Top Level Evaluation Operators 77 | // **********************************************************************************************// 78 | 79 | // Helpers 80 | def $eq[T: BSONWriter](value: T) = $doc("$eq" -> value) 81 | 82 | def $gt[T: BSONWriter](value: T) = $doc("$gt" -> value) 83 | 84 | /** Matches values that are greater than or equal to the value specified in the query. */ 85 | def $gte[T: BSONWriter](value: T) = $doc("$gte" -> value) 86 | 87 | /** Matches any of the values that exist in an array specified in the query. */ 88 | def $in[T: BSONWriter](values: T*) = $doc("$in" -> values) 89 | 90 | /** Matches values that are less than the value specified in the query. */ 91 | def $lt[T: BSONWriter](value: T) = $doc("$lt" -> value) 92 | 93 | /** Matches values that are less than or equal to the value specified in the query. */ 94 | def $lte[T: BSONWriter](value: T) = $doc("$lte" -> value) 95 | 96 | /** Matches all values that are not equal to the value specified in the query. */ 97 | def $ne[T: BSONWriter](value: T) = $doc("$ne" -> value) 98 | 99 | /** Matches values that do not exist in an array specified to the query. */ 100 | def $nin[T: BSONWriter](values: T*) = $doc("$nin" -> values) 101 | 102 | def $exists(value: Boolean) = $doc("$exists" -> value) 103 | 104 | trait CurrentDateValueProducer[T]: 105 | def produce: BSONValue 106 | 107 | implicit final class BooleanCurrentDateValueProducer(value: Boolean) 108 | extends CurrentDateValueProducer[Boolean]: 109 | def produce: BSONValue = BSONBoolean(value) 110 | 111 | implicit final class StringCurrentDateValueProducer(value: String) extends CurrentDateValueProducer[String]: 112 | def isValid: Boolean = Seq("date", "timestamp") contains value 113 | 114 | def produce: BSONValue = 115 | if !isValid then throw new IllegalArgumentException(value) 116 | 117 | $doc("$type" -> value) 118 | 119 | // End of Top Level Field Update Operators 120 | // **********************************************************************************************// 121 | 122 | // **********************************************************************************************// 123 | // Top Level Array Update Operators 124 | 125 | def $pop(item: (String, Int)): Bdoc = 126 | if item._2 != -1 && item._2 != 1 then 127 | throw new IllegalArgumentException(s"${item._2} is not equal to: -1 | 1") 128 | $doc("$pop" -> $doc(item)) 129 | 130 | def $push(item: ElementProducer): Bdoc = 131 | $doc("$push" -> $doc(item)) 132 | 133 | def $pushEach[T: BSONWriter](field: String, values: T*): Bdoc = 134 | $doc( 135 | "$push" -> $doc( 136 | field -> $doc( 137 | "$each" -> values 138 | ) 139 | ) 140 | ) 141 | 142 | def $pull(item: ElementProducer): Bdoc = 143 | $doc("$pull" -> $doc(item)) 144 | 145 | def $addOrPull[T: BSONWriter](key: String, value: T, add: Boolean): Bdoc = 146 | $doc((if add then "$addToSet" else "$pull") -> $doc(key -> value)) 147 | 148 | // End ofTop Level Array Update Operators 149 | // **********************************************************************************************// 150 | 151 | /** Represents the initial state of the expression which has only the name of the field. It does not know 152 | * the value of the expression. 153 | */ 154 | trait ElementBuilder: 155 | def field: String 156 | def append(value: Bdoc): Bdoc = value 157 | 158 | /** Represents the state of an expression which has a field and a value */ 159 | trait Expression[V] extends ElementBuilder: 160 | def value: V 161 | def toBdoc(implicit writer: BSONWriter[V]) = toBSONDocument(this) 162 | 163 | /* 164 | * This type of expressions cannot be cascaded. Examples: 165 | * 166 | * {{{ 167 | * "price" $eq 10 168 | * "price" $ne 1000 169 | * "size" $in ("S", "M", "L") 170 | * "size" $nin ("S", "XXL") 171 | * }}} 172 | * 173 | */ 174 | case class SimpleExpression[V <: BSONValue](field: String, value: V) extends Expression[V] 175 | 176 | /** Expressions of this type can be cascaded. Examples: 177 | * 178 | * {{{ 179 | * "age" $gt 50 $lt 60 180 | * "age" $gte 50 $lte 60 181 | * }}} 182 | */ 183 | case class CompositeExpression(field: String, value: Bdoc) 184 | extends Expression[Bdoc] 185 | with ComparisonOperators: 186 | override def append(value: Bdoc): Bdoc = 187 | this.value ++ value 188 | 189 | /** MongoDB comparison operators. */ 190 | trait ComparisonOperators: 191 | self: ElementBuilder => 192 | 193 | def $eq[T: BSONWriter](value: T): SimpleExpression[BSONValue] = 194 | SimpleExpression(field, implicitly[BSONWriter[T]].writeTry(value).get) 195 | 196 | /** Matches values that are greater than the value specified in the query. */ 197 | def $gt[T: BSONWriter](value: T): CompositeExpression = 198 | CompositeExpression(field, append($doc("$gt" -> value))) 199 | 200 | /** Matches values that are greater than or equal to the value specified in the query. */ 201 | def $gte[T: BSONWriter](value: T): CompositeExpression = 202 | CompositeExpression(field, append($doc("$gte" -> value))) 203 | 204 | /** Matches any of the values that exist in an array specified in the query. */ 205 | def $in[T: BSONWriter](values: Iterable[T]): SimpleExpression[Bdoc] = 206 | SimpleExpression(field, $doc("$in" -> values)) 207 | 208 | /** Matches values that are less than the value specified in the query. */ 209 | def $lt[T: BSONWriter](value: T): CompositeExpression = 210 | CompositeExpression(field, append($doc("$lt" -> value))) 211 | 212 | /** Matches values that are less than or equal to the value specified in the query. */ 213 | def $lte[T: BSONWriter](value: T): CompositeExpression = 214 | CompositeExpression(field, append($doc("$lte" -> value))) 215 | 216 | /** Matches all values that are not equal to the value specified in the query. */ 217 | def $ne[T: BSONWriter](value: T): SimpleExpression[Bdoc] = 218 | SimpleExpression(field, $doc("$ne" -> value)) 219 | 220 | /** Matches values that do not exist in an array specified to the query. */ 221 | def $nin[T: BSONWriter](values: Iterable[T]): SimpleExpression[Bdoc] = 222 | SimpleExpression(field, $doc("$nin" -> values)) 223 | 224 | trait LogicalOperators: 225 | self: ElementBuilder => 226 | def $not(f: String => Expression[Bdoc]): SimpleExpression[Bdoc] = 227 | val expression = f(field) 228 | SimpleExpression(field, $doc("$not" -> expression.value)) 229 | 230 | trait ElementOperators: 231 | self: ElementBuilder => 232 | def $exists(v: Boolean): SimpleExpression[Bdoc] = 233 | SimpleExpression(field, $doc("$exists" -> v)) 234 | 235 | trait EvaluationOperators: 236 | self: ElementBuilder => 237 | def $mod(divisor: Int, remainder: Int): SimpleExpression[Bdoc] = 238 | SimpleExpression(field, $doc("$mod" -> BSONArray(divisor, remainder))) 239 | 240 | def $regex(value: String, options: String = ""): SimpleExpression[BSONRegex] = 241 | SimpleExpression(field, BSONRegex(value, options)) 242 | 243 | def $startsWith(value: String, options: String = ""): SimpleExpression[BSONRegex] = 244 | $regex(s"^$value", options) 245 | 246 | object $sort: 247 | 248 | def asc(field: String) = $doc(field -> 1) 249 | def desc(field: String) = $doc(field -> -1) 250 | 251 | val naturalAsc = asc("$natural") 252 | val naturalDesc = desc("$natural") 253 | val naturalOrder = naturalDesc 254 | 255 | val createdAsc = asc("createdAt") 256 | val createdDesc = desc("createdAt") 257 | val updatedDesc = desc("updatedAt") 258 | 259 | implicit class ElementBuilderLike(val field: String) 260 | extends ElementBuilder 261 | with ComparisonOperators 262 | with ElementOperators 263 | with EvaluationOperators 264 | with LogicalOperators 265 | 266 | import scala.language.implicitConversions 267 | implicit def toBSONDocument[V: BSONWriter](expression: Expression[V]): Bdoc = 268 | $doc(expression.field -> expression.value) 269 | 270 | object dsl extends dsl with Handlers 271 | -------------------------------------------------------------------------------- /src/main/scala/lila/ByteArray.scala: -------------------------------------------------------------------------------- 1 | package lila.db 2 | 3 | import scala.util.Try 4 | 5 | import reactivemongo.api.bson.* 6 | 7 | case class ByteArray(value: Array[Byte]): 8 | 9 | def isEmpty = value.lengthIs == 0 10 | 11 | def toHexStr = ByteArray.hex.hex2Str(value) 12 | 13 | def showBytes: String = 14 | value 15 | .map { b => 16 | "%08d".format({ b & 0xff }.toBinaryString.toInt) 17 | } 18 | .mkString(",") 19 | 20 | override def toString = toHexStr 21 | 22 | object ByteArray: 23 | 24 | val empty = ByteArray(Array()) 25 | 26 | def fromHexStr(hexStr: String): Try[ByteArray] = 27 | Try(ByteArray(hex.str2Hex(hexStr))) 28 | 29 | given byteArrayHandler: BSONHandler[ByteArray] = dsl.quickHandler[ByteArray]( 30 | { case v: BSONBinary => ByteArray(v.byteArray) }, 31 | v => BSONBinary(v.value, subtype) 32 | ) 33 | 34 | given Conversion[Array[Byte], ByteArray] = ByteArray(_) 35 | 36 | def parseBytes(s: List[String]) = ByteArray(s.map(parseByte).toArray) 37 | 38 | private def parseByte(s: String): Byte = 39 | var i = s.length - 1 40 | var sum = 0 41 | var mult = 1 42 | while i >= 0 do 43 | s.charAt(i) match 44 | case '1' => sum += mult 45 | case '0' => 46 | case x => sys.error(s"invalid binary literal: $x in $s") 47 | mult *= 2 48 | i -= 1 49 | sum.toByte 50 | 51 | // from https://github.com/ReactiveMongo/ReactiveMongo-BSON/blob/master/api/src/main/scala/Digest.scala 52 | private object hex: 53 | 54 | private val HEX_CHARS: Array[Char] = "0123456789abcdef".toCharArray 55 | 56 | /** Turns a hexadecimal String into an array of Byte. */ 57 | def str2Hex(str: String): Array[Byte] = 58 | val sz = str.length / 2 59 | val bytes = new Array[Byte](sz) 60 | 61 | var i = 0 62 | while i < sz do 63 | val t = 2 * i 64 | bytes(i) = Integer.parseInt(str.substring(t, t + 2), 16).toByte 65 | i += 1 66 | 67 | bytes 68 | 69 | /** Turns an array of Byte into a String representation in hexadecimal. */ 70 | def hex2Str(bytes: Array[Byte]): String = 71 | val len = bytes.length 72 | val hex = new Array[Char](2 * len) 73 | 74 | var i = 0 75 | while i < len do 76 | val b = bytes(i) 77 | 78 | val t = 2 * i // index in output buffer 79 | 80 | hex(t) = HEX_CHARS((b & 0xf0) >>> 4) 81 | hex(t + 1) = HEX_CHARS(b & 0x0f) 82 | 83 | i = i + 1 84 | 85 | new String(hex) 86 | 87 | def subtype = Subtype.GenericBinarySubtype 88 | -------------------------------------------------------------------------------- /src/main/scala/lila/CorrespondenceClock.scala: -------------------------------------------------------------------------------- 1 | package lila.game 2 | 3 | import chess.Color 4 | 5 | // times are expressed in seconds 6 | case class CorrespondenceClock( 7 | increment: Int, 8 | whiteTime: Float, 9 | blackTime: Float 10 | ): 11 | 12 | def daysPerTurn = increment / 60 / 60 / 24 13 | 14 | def emerg = 60 * 10 15 | 16 | def remainingTime(c: Color) = c.fold(whiteTime, blackTime) 17 | 18 | def outoftime(c: Color) = remainingTime(c) == 0 19 | 20 | // in seconds 21 | def estimateTotalTime = increment * 40 / 2 22 | 23 | def incrementHours = increment / 60 / 60 24 | -------------------------------------------------------------------------------- /src/main/scala/lila/Eval.scala: -------------------------------------------------------------------------------- 1 | package lila.tree 2 | 3 | import chess.format.Uci 4 | 5 | case class Eval( 6 | cp: Option[Eval.Cp], 7 | mate: Option[Eval.Mate], 8 | best: Option[Uci.Move] 9 | ): 10 | 11 | def isEmpty = cp.isEmpty && mate.isEmpty 12 | 13 | def dropBest = copy(best = None) 14 | 15 | def invert = copy(cp = cp.map(_.invert), mate = mate.map(_.invert)) 16 | 17 | object Eval: 18 | 19 | case class Score(value: Either[Cp, Mate]) extends AnyVal: 20 | 21 | def cp: Option[Cp] = value.left.toOption 22 | def mate: Option[Mate] = value.right.toOption 23 | 24 | def isCheckmate = value == Score.checkmate 25 | def mateFound = value.isRight 26 | 27 | def invert = copy(value = value.left.map(_.invert).right.map(_.invert)) 28 | def invertIf(cond: Boolean) = if cond then invert else this 29 | 30 | object Score: 31 | 32 | def cp(x: Cp): Score = Score(Left(x)) 33 | def mate(y: Mate): Score = Score(Right(y)) 34 | 35 | val checkmate: Either[Cp, Mate] = Right(Mate(0)) 36 | 37 | case class Cp(value: Int) extends AnyVal with Ordered[Cp]: 38 | 39 | def centipawns = value 40 | 41 | def pawns: Float = value / 100f 42 | def showPawns: String = "%.2f".format(pawns) 43 | 44 | def ceiled = 45 | if value > Cp.CEILING then Cp(Cp.CEILING) 46 | else if value < -Cp.CEILING then Cp(-Cp.CEILING) 47 | else this 48 | 49 | def invert = Cp(value = -value) 50 | def invertIf(cond: Boolean) = if cond then invert else this 51 | 52 | def compare(other: Cp) = value.compare(other.value) 53 | 54 | def signum: Int = Math.signum(value).toInt 55 | 56 | object Cp: 57 | 58 | val CEILING = 1000 59 | 60 | val initial = Cp(15) 61 | 62 | case class Mate(value: Int) extends AnyVal with Ordered[Mate]: 63 | 64 | def moves = value 65 | 66 | def invert = Mate(value = -value) 67 | def invertIf(cond: Boolean) = if cond then invert else this 68 | 69 | def compare(other: Mate) = value.compare(other.value) 70 | 71 | def signum: Int = Math.signum(value).toInt 72 | 73 | def positive = value > 0 74 | def negative = value < 0 75 | 76 | val initial = Eval(Some(Cp.initial), None, None) 77 | 78 | val empty = Eval(None, None, None) 79 | -------------------------------------------------------------------------------- /src/main/scala/lila/PerfType.scala: -------------------------------------------------------------------------------- 1 | package lila.rating 2 | 3 | import chess.Speed 4 | 5 | sealed abstract class PerfType( 6 | val id: Int, 7 | val key: String, 8 | val name: String, 9 | val title: String, 10 | val iconChar: Char 11 | ): 12 | 13 | def iconString = iconChar.toString 14 | 15 | object PerfType: 16 | 17 | case object UltraBullet 18 | extends PerfType( 19 | 0, 20 | key = "ultraBullet", 21 | name = Speed.UltraBullet.name, 22 | title = Speed.UltraBullet.title, 23 | iconChar = '{' 24 | ) 25 | 26 | case object Bullet 27 | extends PerfType( 28 | 1, 29 | key = "bullet", 30 | name = Speed.Bullet.name, 31 | title = Speed.Bullet.title, 32 | iconChar = 'T' 33 | ) 34 | 35 | case object Blitz 36 | extends PerfType( 37 | 2, 38 | key = "blitz", 39 | name = Speed.Blitz.name, 40 | title = Speed.Blitz.title, 41 | iconChar = ')' 42 | ) 43 | 44 | case object Rapid 45 | extends PerfType( 46 | 6, 47 | key = "rapid", 48 | name = Speed.Rapid.name, 49 | title = Speed.Rapid.title, 50 | iconChar = '#' 51 | ) 52 | 53 | case object Classical 54 | extends PerfType( 55 | 3, 56 | key = "classical", 57 | name = Speed.Classical.name, 58 | title = Speed.Classical.title, 59 | iconChar = '+' 60 | ) 61 | 62 | case object Correspondence 63 | extends PerfType( 64 | 4, 65 | key = "correspondence", 66 | name = "Correspondence", 67 | title = "Correspondence (days per turn)", 68 | iconChar = ';' 69 | ) 70 | 71 | case object Standard 72 | extends PerfType( 73 | 5, 74 | key = "standard", 75 | name = chess.variant.Standard.name, 76 | title = "Standard rules of chess", 77 | iconChar = '8' 78 | ) 79 | 80 | case object Chess960 81 | extends PerfType( 82 | 11, 83 | key = "chess960", 84 | name = chess.variant.Chess960.name, 85 | title = "Chess960 variant", 86 | iconChar = '\'' 87 | ) 88 | 89 | case object KingOfTheHill 90 | extends PerfType( 91 | 12, 92 | key = "kingOfTheHill", 93 | name = chess.variant.KingOfTheHill.name, 94 | title = "King of the Hill variant", 95 | iconChar = '(' 96 | ) 97 | 98 | case object Antichess 99 | extends PerfType( 100 | 13, 101 | key = "antichess", 102 | name = chess.variant.Antichess.name, 103 | title = "Antichess variant", 104 | iconChar = '@' 105 | ) 106 | 107 | case object Atomic 108 | extends PerfType( 109 | 14, 110 | key = "atomic", 111 | name = chess.variant.Atomic.name, 112 | title = "Atomic variant", 113 | iconChar = '>' 114 | ) 115 | 116 | case object ThreeCheck 117 | extends PerfType( 118 | 15, 119 | key = "threeCheck", 120 | name = chess.variant.ThreeCheck.name, 121 | title = "Three-check variant", 122 | iconChar = '.' 123 | ) 124 | 125 | case object Horde 126 | extends PerfType( 127 | 16, 128 | key = "horde", 129 | name = chess.variant.Horde.name, 130 | title = "Horde variant", 131 | iconChar = '_' 132 | ) 133 | 134 | case object RacingKings 135 | extends PerfType( 136 | 17, 137 | key = "racingKings", 138 | name = chess.variant.RacingKings.name, 139 | title = "Racing kings variant", 140 | iconChar = '' 141 | ) 142 | 143 | case object Crazyhouse 144 | extends PerfType( 145 | 18, 146 | key = "crazyhouse", 147 | name = chess.variant.Crazyhouse.name, 148 | title = "Crazyhouse variant", 149 | iconChar = '' 150 | ) 151 | 152 | case object Puzzle 153 | extends PerfType( 154 | 20, 155 | key = "puzzle", 156 | name = "Training", 157 | title = "Training puzzles", 158 | iconChar = '-' 159 | ) 160 | 161 | val all: List[PerfType] = List( 162 | UltraBullet, 163 | Bullet, 164 | Blitz, 165 | Rapid, 166 | Classical, 167 | Correspondence, 168 | Standard, 169 | Crazyhouse, 170 | Chess960, 171 | KingOfTheHill, 172 | ThreeCheck, 173 | Antichess, 174 | Atomic, 175 | Horde, 176 | RacingKings, 177 | Puzzle 178 | ) 179 | val byKey = all.view.map { p => 180 | (p.key, p) 181 | }.toMap 182 | val byId = all.view.map { p => 183 | (p.id, p) 184 | }.toMap 185 | 186 | val default = Standard 187 | 188 | def apply(key: String): Option[PerfType] = byKey.get(key) 189 | def orDefault(key: String): PerfType = apply(key).getOrElse(default) 190 | 191 | def apply(id: Int): Option[PerfType] = byId.get(id) 192 | 193 | def name(key: String): Option[String] = apply(key).map(_.name) 194 | 195 | def id2key(id: Int): Option[String] = byId.get(id).map(_.key) 196 | 197 | val nonPuzzle: List[PerfType] = List( 198 | UltraBullet, 199 | Bullet, 200 | Blitz, 201 | Rapid, 202 | Classical, 203 | Correspondence, 204 | Crazyhouse, 205 | Chess960, 206 | KingOfTheHill, 207 | ThreeCheck, 208 | Antichess, 209 | Atomic, 210 | Horde, 211 | RacingKings 212 | ) 213 | val nonGame: List[PerfType] = List(Puzzle) 214 | val leaderboardable: List[PerfType] = List( 215 | Bullet, 216 | Blitz, 217 | Rapid, 218 | Classical, 219 | UltraBullet, 220 | Crazyhouse, 221 | Chess960, 222 | KingOfTheHill, 223 | ThreeCheck, 224 | Antichess, 225 | Atomic, 226 | Horde, 227 | RacingKings 228 | ) 229 | val variants: List[PerfType] = List( 230 | Crazyhouse, 231 | Chess960, 232 | KingOfTheHill, 233 | ThreeCheck, 234 | Antichess, 235 | Atomic, 236 | Horde, 237 | RacingKings 238 | ) 239 | val standard: List[PerfType] = 240 | List(Bullet, Blitz, Rapid, Classical, Correspondence) 241 | 242 | def isGame(pt: PerfType) = !nonGame.contains(pt) 243 | -------------------------------------------------------------------------------- /src/main/scala/lila/Sequence.scala: -------------------------------------------------------------------------------- 1 | package lila.common 2 | 3 | object Sequence: 4 | def interleave[A](a: Seq[A], b: Seq[A]): Vector[A] = 5 | val iterA = a.iterator 6 | val iterB = b.iterator 7 | val builder = Vector.newBuilder[A] 8 | while iterA.hasNext && iterB.hasNext do builder += iterA.next += iterB.next 9 | builder ++= iterA ++= iterB 10 | 11 | builder.result 12 | -------------------------------------------------------------------------------- /src/main/scala/lila/analyse/Advice.scala: -------------------------------------------------------------------------------- 1 | package lila.analyse 2 | 3 | import chess.format.pgn.Glyph 4 | import lila.tree.Eval.* 5 | 6 | sealed trait Advice: 7 | def judgment: Advice.Judgment 8 | def info: Info 9 | def prev: Info 10 | 11 | def ply = info.ply 12 | def turn = info.turn 13 | def color = info.color 14 | def cp = info.cp 15 | def mate = info.mate 16 | 17 | def makeComment(withEval: Boolean, withBestMove: Boolean): String = 18 | evalComment.filter(_ => withEval).fold("") { c => 19 | s"($c) " 20 | } + 21 | (this match 22 | case MateAdvice(seq, _, _, _) => seq.desc 23 | case CpAdvice(judgment, _, _) => judgment.toString) + "." + { 24 | info.variation.headOption.filter(_ => withBestMove).fold("") { move => 25 | s" Best move was $move." 26 | } 27 | } 28 | 29 | def evalComment: Option[String] = 30 | Some { 31 | List(prev.evalComment, info.evalComment).flatten.mkString(" → ") 32 | }.filter(_.nonEmpty) 33 | 34 | object Advice: 35 | 36 | sealed abstract class Judgment(val glyph: Glyph, val name: String): 37 | override def toString = name 38 | def isBlunder = this == Judgment.Blunder 39 | object Judgment: 40 | object Inaccuracy extends Judgment(Glyph.MoveAssessment.dubious, "Inaccuracy") 41 | object Mistake extends Judgment(Glyph.MoveAssessment.mistake, "Mistake") 42 | object Blunder extends Judgment(Glyph.MoveAssessment.blunder, "Blunder") 43 | val all = List(Inaccuracy, Mistake, Blunder) 44 | 45 | def apply(prev: Info, info: Info): Option[Advice] = CpAdvice(prev, info).orElse(MateAdvice(prev, info)) 46 | 47 | private[analyse] case class CpAdvice(judgment: Advice.Judgment, info: Info, prev: Info) extends Advice 48 | 49 | private[analyse] object CpAdvice: 50 | 51 | private val cpJudgments = 52 | List(300 -> Advice.Judgment.Blunder, 100 -> Advice.Judgment.Mistake, 50 -> Advice.Judgment.Inaccuracy) 53 | 54 | def apply(prev: Info, info: Info): Option[CpAdvice] = 55 | for 56 | cp <- prev.cp.map(_.ceiled.centipawns) 57 | infoCp <- info.cp.map(_.ceiled.centipawns) 58 | delta = 59 | val d = infoCp - cp 60 | info.color.fold(-d, d) 61 | judgment <- cpJudgments.find { (d, _) => d <= delta }.map(_._2) 62 | yield CpAdvice(judgment, info, prev) 63 | 64 | sealed abstract private[analyse] class MateSequence(val desc: String) 65 | private[analyse] case object MateDelayed extends MateSequence(desc = "Not the best checkmate sequence") 66 | private[analyse] case object MateLost extends MateSequence(desc = "Lost forced checkmate sequence") 67 | private[analyse] case object MateCreated extends MateSequence(desc = "Checkmate is now unavoidable") 68 | 69 | private[analyse] object MateSequence: 70 | def apply(prev: Option[Mate], next: Option[Mate]): Option[MateSequence] = 71 | Some(prev, next).collect { 72 | case (None, Some(n)) if n.negative => MateCreated 73 | case (Some(p), None) if p.positive => MateLost 74 | case (Some(p), Some(n)) if p.positive && n.negative => MateLost 75 | case (Some(p), Some(n)) if p.positive && n >= p && p <= Mate(5) => MateDelayed 76 | } 77 | private[analyse] case class MateAdvice( 78 | sequence: MateSequence, 79 | judgment: Advice.Judgment, 80 | info: Info, 81 | prev: Info 82 | ) extends Advice 83 | private[analyse] object MateAdvice: 84 | 85 | def apply(prev: Info, info: Info): Option[MateAdvice] = 86 | def invertCp(cp: Cp) = cp.invertIf(info.color.black) 87 | def invertMate(mate: Mate) = mate.invertIf(info.color.black) 88 | def prevCp = prev.cp.map(invertCp).fold(0)(_.centipawns) 89 | def nextCp = info.cp.map(invertCp).fold(0)(_.centipawns) 90 | MateSequence(prev.mate.map(invertMate), info.mate.map(invertMate)).map { sequence => 91 | import Advice.Judgment.* 92 | val judgment = sequence match 93 | case MateCreated if prevCp < -999 => Inaccuracy 94 | case MateCreated if prevCp < -700 => Mistake 95 | case MateCreated => Blunder 96 | case MateLost if nextCp > 999 => Inaccuracy 97 | case MateLost if nextCp > 700 => Mistake 98 | case MateLost => Blunder 99 | case MateDelayed => Inaccuracy 100 | MateAdvice(sequence, judgment, info, prev) 101 | } 102 | -------------------------------------------------------------------------------- /src/main/scala/lila/analyse/Analysis.scala: -------------------------------------------------------------------------------- 1 | package lila 2 | package analyse 3 | 4 | import chess.Color 5 | 6 | case class Analysis( 7 | id: String, 8 | infos: List[Info], 9 | startPly: Int, 10 | uid: Option[String], // requester lichess ID 11 | by: Option[String], // analyser lichess ID 12 | date: Instant 13 | ): 14 | 15 | type InfoAdvices = List[(Info, Option[Advice])] 16 | 17 | lazy val infoAdvices: InfoAdvices = { 18 | (Info.start(startPly) :: infos).sliding(2).collect { case List(prev, info) => 19 | info -> { 20 | if info.hasVariation then Advice(prev, info) else None 21 | } 22 | } 23 | }.toList 24 | 25 | lazy val advices: List[Advice] = infoAdvices.flatMap(_._2) 26 | 27 | // ply -> UCI 28 | def bestMoves: Map[Int, String] = 29 | infos.view.flatMap { i => 30 | i.best.map { b => 31 | i.ply -> b.keys 32 | } 33 | }.toMap 34 | 35 | def summary: List[(Color, List[(Advice.Judgment, Int)])] = Color.all.map { color => 36 | color -> (Advice.Judgment.all.map { judgment => 37 | judgment -> (advices.count { adv => 38 | adv.color == color && adv.judgment == judgment 39 | }) 40 | }) 41 | } 42 | 43 | def valid = infos.nonEmpty 44 | 45 | def nbEmptyInfos = infos.count(_.isEmpty) 46 | def emptyRatio: Double = nbEmptyInfos.toDouble / infos.size 47 | 48 | object Analysis: 49 | 50 | import lila.db.BSON 51 | 52 | type ID = String 53 | 54 | given analysisBSONHandler: BSON[Analysis] = new: 55 | def reads(r: BSON.Reader) = 56 | val startPly = r.intD("ply") 57 | val raw = r.str("data") 58 | Analysis( 59 | id = r.str("_id"), 60 | infos = Info.decodeList(raw, startPly).getOrElse { 61 | sys.error(s"Invalid analysis data $raw") 62 | }, 63 | startPly = startPly, 64 | uid = r.strO("uid"), 65 | by = r.strO("by"), 66 | date = r.date("date") 67 | ) 68 | -------------------------------------------------------------------------------- /src/main/scala/lila/analyse/Annotator.scala: -------------------------------------------------------------------------------- 1 | package lila 2 | package analyse 3 | 4 | import chess.format.pgn.{ Glyphs, Pgn } 5 | import chess.Ply 6 | 7 | object Annotator: 8 | 9 | def apply( 10 | p: Pgn, 11 | analysis: Option[Analysis] 12 | ): Pgn = 13 | addEvals( 14 | addGlyphs(p, analysis.fold(List.empty[Advice])(_.advices)), 15 | analysis.fold(List.empty[Info])(_.infos) 16 | ) 17 | 18 | private def addGlyphs(p: Pgn, advices: List[Advice]): Pgn = 19 | advices.foldLeft(p) { (pgn, advice) => 20 | pgn 21 | .modifyInMainline( 22 | Ply(advice.ply), 23 | node => node.copy(value = node.value.copy(glyphs = Glyphs.fromList(advice.judgment.glyph :: Nil))) 24 | ) 25 | .getOrElse(pgn) 26 | } 27 | 28 | private def addEvals(p: Pgn, infos: List[Info]): Pgn = 29 | infos.foldLeft(p) { (pgn, info) => 30 | pgn 31 | .updatePly( 32 | Ply(info.ply), 33 | move => move.copy(comments = info.pgnComment.toList ::: move.comments) 34 | ) 35 | .getOrElse(pgn) 36 | } 37 | -------------------------------------------------------------------------------- /src/main/scala/lila/analyse/Info.scala: -------------------------------------------------------------------------------- 1 | package lila 2 | package analyse 3 | 4 | import chess.Color 5 | import chess.format.Uci 6 | import chess.format.pgn.Comment 7 | 8 | import lila.tree.Eval 9 | 10 | case class Info( 11 | ply: Int, 12 | eval: Eval, 13 | // variation is first in UCI, then converted to PGN before storage 14 | variation: List[String] = Nil 15 | ): 16 | 17 | def cp = eval.cp 18 | def mate = eval.mate 19 | def best = eval.best 20 | 21 | def turn = 1 + (ply - 1) / 2 22 | 23 | def color = Color.fromWhite(ply % 2 == 1) 24 | 25 | def hasVariation = variation.nonEmpty 26 | def dropVariation = copy(variation = Nil, eval = eval.dropBest) 27 | 28 | def invert = copy(eval = eval.invert) 29 | 30 | def cpComment: Option[String] = cp.map(_.showPawns) 31 | def mateComment: Option[String] = mate.map { m => 32 | s"Mate in ${math.abs(m.value)}" 33 | } 34 | def evalComment: Option[String] = cpComment.orElse(mateComment) 35 | 36 | def pgnComment = Comment.from( 37 | cp 38 | .map(_.pawns.toString) 39 | .orElse(mate.map(m => s"#${m.value}")) 40 | .map(c => s"[%eval $c]") 41 | ) 42 | 43 | def isEmpty = cp.isEmpty && mate.isEmpty 44 | 45 | def forceCentipawns: Option[Int] = mate match 46 | case None => cp.map(_.centipawns) 47 | case Some(m) if m.negative => Some(Int.MinValue - m.value) 48 | case Some(m) => Some(Int.MaxValue - m.value) 49 | 50 | object Info: 51 | 52 | import Eval.{ Cp, Mate } 53 | 54 | val LineMaxPlies = 14 55 | 56 | private val separator = "," 57 | private val listSeparator = ";" 58 | 59 | def start(ply: Int) = Info(ply, Eval.initial, Nil) 60 | 61 | private def strCp(s: String) = s.toIntOption.map(Cp.apply) 62 | private def strMate(s: String) = s.toIntOption.map(Mate.apply) 63 | 64 | private def decode(ply: Int, str: String): Option[Info] = str.split(separator) match 65 | case Array() => Some(Info(ply, Eval.empty)) 66 | case Array(cp) => Some(Info(ply, Eval(strCp(cp), None, None))) 67 | case Array(cp, ma) => Some(Info(ply, Eval(strCp(cp), strMate(ma), None))) 68 | case Array(cp, ma, va) => Some(Info(ply, Eval(strCp(cp), strMate(ma), None), va.split(' ').toList)) 69 | case Array(cp, ma, va, be) => 70 | Some(Info(ply, Eval(strCp(cp), strMate(ma), Uci.Move.fromChars(be)), va.split(' ').toList)) 71 | case _ => None 72 | 73 | def decodeList(str: String, fromPly: Int): Option[List[Info]] = { 74 | str.split(listSeparator).toList.zipWithIndex.map { case (infoStr, index) => 75 | decode(index + 1 + fromPly, infoStr) 76 | } 77 | }.sequence 78 | 79 | def apply(cp: Option[Cp], mate: Option[Mate], variation: List[String]): Int => Info = 80 | ply => Info(ply, Eval(cp, mate, None), variation) 81 | -------------------------------------------------------------------------------- /src/main/scala/lila/game/BSONHandlers.scala: -------------------------------------------------------------------------------- 1 | package lila 2 | package game 3 | 4 | import chess.variant.{ Crazyhouse, Variant } 5 | import chess.{ 6 | ByColor, 7 | CheckCount, 8 | Clock, 9 | Color, 10 | Game as ChessGame, 11 | HalfMoveClock, 12 | History as ChessHistory, 13 | Mode, 14 | Ply, 15 | Status, 16 | UnmovedRooks 17 | } 18 | import lila.db.BSON 19 | import lila.db.dsl.{ *, given } 20 | import reactivemongo.api.bson.* 21 | import scala.util.Try 22 | 23 | object BSONHandlers: 24 | import lila.db.ByteArray.byteArrayHandler 25 | 26 | given StatusBSONHandler: BSONHandler[Status] = tryHandler[Status]( 27 | { case BSONInteger(v) => 28 | Status(v) 29 | .fold[Try[Status]](scala.util.Failure(new Exception(s"No such status: $v")))(scala.util.Success.apply) 30 | }, 31 | x => BSONInteger(x.id) 32 | ) 33 | 34 | private[game] given unmovedRooksHandler: BSONHandler[UnmovedRooks] = tryHandler[UnmovedRooks]( 35 | { case bin: BSONBinary => byteArrayHandler.readTry(bin).map(BinaryFormat.unmovedRooks.read) }, 36 | x => byteArrayHandler.writeTry(BinaryFormat.unmovedRooks.write(x)).get 37 | ) 38 | 39 | private[game] given crazyhouseDataHandler: BSON[Crazyhouse.Data]: 40 | import Crazyhouse.* 41 | def reads(r: BSON.Reader) = 42 | val (white, black) = r.str("p").view.flatMap(chess.Piece.fromChar).to(List).partition(_.is(chess.White)) 43 | Crazyhouse.Data( 44 | pockets = ByColor(white, black).map(pieces => Pocket(pieces.map(_.role))), 45 | promoted = chess.Bitboard(r.str("t").view.flatMap(chess.Square.fromChar(_))) 46 | ) 47 | 48 | private given lightGameReader: lila.db.BSONReadOnly[LightGame]: 49 | 50 | import Game.BSONFields as F 51 | 52 | private val emptyPlayerBuilder = LightPlayer.builderRead($empty) 53 | 54 | def reads(r: BSON.Reader): LightGame = 55 | val winC = r.boolO(F.winnerColor).map { Color.fromWhite(_) } 56 | val uids = ~r.getO[List[String]](F.playerUids) 57 | val (whiteUid, blackUid) = (uids.headOption.filter(_.nonEmpty), uids.lift(1)) 58 | def makePlayer(field: String, color: Color, uid: Option[String]): LightPlayer = 59 | val builder = 60 | r.getO[LightPlayer.Builder](field)(using LightPlayer.lightPlayerReader) | emptyPlayerBuilder 61 | builder(color)(uid) 62 | LightGame( 63 | id = r.str(F.id), 64 | whitePlayer = makePlayer(F.whitePlayer, Color.White, whiteUid), 65 | blackPlayer = makePlayer(F.blackPlayer, Color.Black, blackUid), 66 | status = r.get[Status](F.status), 67 | win = winC 68 | ) 69 | 70 | given gameBSONHandler: BSON[Game] = new: 71 | 72 | import Game.BSONFields as F 73 | 74 | def reads(r: BSON.Reader): Game = 75 | 76 | val playerIds = r.str(F.playerIds) 77 | val light = lightGameReader.reads(r) 78 | 79 | val gameVariant = Variant.idOrDefault(r.getO[Variant.Id](F.variant)) 80 | val startedAtPly = Ply(r.intD(F.startedAtTurn)) 81 | val ply = r.get[Ply](F.turns).atMost(600) 82 | val turnColor = ply.turn 83 | val createdAt = r.date(F.createdAt) 84 | val playedPlies = ply - startedAtPly 85 | 86 | val whitePlayer = Player.from(light, Color.white, playerIds, r.getD[Bdoc](F.whitePlayer)) 87 | val blackPlayer = Player.from(light, Color.black, playerIds, r.getD[Bdoc](F.blackPlayer)) 88 | 89 | val decoded = r.bytesO(F.huffmanPgn) match 90 | case Some(huffPgn) => PgnStorage.Huffman.decode(huffPgn, playedPlies, light.id) 91 | case None => 92 | val clm = r.get[CastleLastMove](F.castleLastMove) 93 | val sans = PgnStorage.OldBin.decode(r.bytesD(F.oldPgn), playedPlies) 94 | val halfMoveClock = 95 | HalfMoveClock.from( 96 | sans.reverse 97 | .indexWhere(san => san.value.contains("x") || san.value.headOption.exists(_.isLower)) 98 | .some 99 | .filter(HalfMoveClock.initial.value <= _) 100 | ) 101 | PgnStorage.Decoded( 102 | sans = sans, 103 | board = chess.Board.fromMap(BinaryFormat.piece.read(r.bytes(F.binaryPieces), gameVariant)), 104 | positionHashes = 105 | r.getO[Array[Byte]](F.positionHashes).map(chess.PositionHash.apply) | chess.PositionHash.empty, 106 | unmovedRooks = r.getO[UnmovedRooks](F.unmovedRooks) | UnmovedRooks.default, 107 | lastMove = clm.lastMove, 108 | castles = clm.castles, 109 | halfMoveClock = halfMoveClock 110 | .orElse(r.getO[chess.format.Fen.Full](F.initialFen).flatMap { fen => 111 | chess.format.Fen.readHalfMoveClockAndFullMoveNumber(fen)._1 112 | }) 113 | .getOrElse(playedPlies.into(HalfMoveClock)) 114 | ) 115 | 116 | val chessGame = ChessGame( 117 | board = chess.Position( 118 | board = decoded.board, 119 | history = ChessHistory( 120 | lastMove = decoded.lastMove, 121 | castles = decoded.castles, 122 | halfMoveClock = decoded.halfMoveClock, 123 | positionHashes = decoded.positionHashes, 124 | unmovedRooks = decoded.unmovedRooks, 125 | checkCount = if gameVariant.threeCheck then 126 | val counts = r.intsD(F.checkCount) 127 | CheckCount(~counts.headOption, ~counts.lastOption) 128 | else Game.emptyCheckCount, 129 | crazyData = gameVariant.crazyhouse.option(r.get[Crazyhouse.Data](F.crazyData)) 130 | ), 131 | variant = gameVariant, 132 | color = turnColor 133 | ), 134 | sans = decoded.sans, 135 | clock = r 136 | .getO[Color => Clock](F.clock)(using 137 | clockBSONReader(createdAt, whitePlayer.berserk, blackPlayer.berserk) 138 | ) 139 | .map(_(turnColor)), 140 | ply = ply, 141 | startedAtPly = startedAtPly 142 | ) 143 | 144 | val whiteClockHistory = r.bytesO(F.whiteClockHistory) 145 | val blackClockHistory = r.bytesO(F.blackClockHistory) 146 | 147 | Game( 148 | id = light.id, 149 | players = ByColor(whitePlayer, blackPlayer), 150 | chess = chessGame, 151 | clockHistory = for 152 | clk <- chessGame.clock 153 | bw <- whiteClockHistory 154 | bb <- blackClockHistory 155 | history <- BinaryFormat.clockHistory 156 | .read(clk.limit, bw, bb, (light.status == Status.Outoftime).option(turnColor)) 157 | yield history, 158 | status = light.status, 159 | daysPerTurn = r.getO[Int](F.daysPerTurn), 160 | binaryMoveTimes = r.bytesO(F.moveTimes), 161 | mode = Mode(r.boolD(F.rated)), 162 | createdAt = createdAt, 163 | movedAt = r.dateD(F.movedAt, createdAt), 164 | metadata = Metadata( 165 | source = r.intO(F.source).flatMap(Source.apply), 166 | tournamentId = r.strO(F.tournamentId), 167 | swissId = r.strO(F.swissId), 168 | simulId = r.strO(F.simulId), 169 | analysed = r.boolD(F.analysed) 170 | ) 171 | ) 172 | 173 | import chess.format.Fen 174 | given gameWithInitialFenBSONHandler: BSON[Game.WithInitialFen] = new: 175 | def reads(r: BSON.Reader): Game.WithInitialFen = 176 | Game.WithInitialFen( 177 | gameBSONHandler.reads(r), 178 | Fen.Full.from(r.strO(Game.BSONFields.initialFen)) 179 | ) 180 | 181 | private[game] def clockBSONReader(since: Instant, whiteBerserk: Boolean, blackBerserk: Boolean) = 182 | new BSONReader[Color => Clock]: 183 | def readTry(bson: BSONValue): Try[Color => Clock] = 184 | bson match 185 | case bin: BSONBinary => 186 | byteArrayHandler.readTry(bin).map { cl => 187 | BinaryFormat.clock(since).read(cl, whiteBerserk, blackBerserk) 188 | } 189 | case b => lila.db.BSON.handlerBadType(b) 190 | -------------------------------------------------------------------------------- /src/main/scala/lila/game/BinaryFormat.scala: -------------------------------------------------------------------------------- 1 | package lila 2 | package game 3 | 4 | import scala.util.Try 5 | import scala.language.postfixOps 6 | 7 | import chess.variant.Variant 8 | import chess.format.pgn.SanStr 9 | import chess.* 10 | import chess.format.Uci 11 | import org.lichess.compression.clock.Encoder as ClockEncoder 12 | 13 | import lila.db.ByteArray 14 | 15 | object BinaryFormat: 16 | 17 | object pgn: 18 | 19 | def write(moves: Vector[SanStr]): ByteArray = ByteArray: 20 | format.pgn.Binary.writeMoves(moves).get 21 | 22 | def read(ba: ByteArray): Vector[SanStr] = 23 | format.pgn.Binary.readMoves(ba.value.toList).get.toVector 24 | 25 | def read(ba: ByteArray, nb: Int): Vector[SanStr] = 26 | format.pgn.Binary.readMoves(ba.value.toList, nb).get.toVector 27 | 28 | object clockHistory: 29 | 30 | def writeSide(start: Centis, times: Vector[Centis], flagged: Boolean) = 31 | val timesToWrite = if flagged then times.dropRight(1) else times 32 | ByteArray(ClockEncoder.encode(Centis.raw(timesToWrite).to(Array), start.centis)) 33 | 34 | def readSide(start: Centis, ba: ByteArray, flagged: Boolean) = 35 | val decoded: Vector[Centis] = 36 | Centis.from(ClockEncoder.decode(ba.value, start.centis).to(Vector)) 37 | if flagged then decoded :+ Centis(0) else decoded 38 | 39 | def read(start: Centis, bw: ByteArray, bb: ByteArray, flagged: Option[Color]) = 40 | Try { 41 | ClockHistory( 42 | readSide(start, bw, flagged.has(White)), 43 | readSide(start, bb, flagged.has(Black)) 44 | ) 45 | }.fold( 46 | e => throw e, 47 | Some(_) 48 | ) 49 | 50 | object moveTime: 51 | 52 | private type MT = Int // centiseconds 53 | private val size = 16 54 | private val buckets = 55 | List(10, 50, 100, 150, 200, 300, 400, 500, 600, 800, 1000, 1500, 2000, 3000, 4000, 6000) 56 | private val encodeCutoffs = buckets 57 | .zip(buckets.tail) 58 | .map { (i1, i2) => 59 | (i1 + i2) / 2 60 | } 61 | .toVector 62 | 63 | private val decodeMap: Map[Int, MT] = buckets.mapWithIndex((x, i) => i -> x).toMap 64 | 65 | def write(mts: Vector[Centis]): ByteArray = ByteArray: 66 | def enc(mt: Centis) = encodeCutoffs.search(mt.centis).insertionPoint 67 | mts 68 | .grouped(2) 69 | .map: 70 | case Vector(a, b) => (enc(a) << 4) + enc(b) 71 | case Vector(a) => enc(a) << 4 72 | case v => sys.error(s"moveTime.write unexpected $v") 73 | .map(_.toByte) 74 | .toArray 75 | 76 | def read(ba: ByteArray, turns: Ply): Vector[Centis] = Centis.from({ 77 | def dec(x: Int) = decodeMap.getOrElse(x, decodeMap(size - 1)) 78 | ba.value.map(toInt).flatMap { k => 79 | Array(dec(k >> 4), dec(k & 15)) 80 | } 81 | }.view.take(turns.value).toVector) 82 | 83 | final class clock(start: Timestamp): 84 | 85 | def legacyElapsed(clock: Clock, color: Color) = 86 | clock.limit - clock.players(color).remaining 87 | 88 | def computeRemaining(config: Clock.Config, legacyElapsed: Centis) = 89 | config.limit - legacyElapsed 90 | 91 | def write(clock: Clock): ByteArray = ByteArray { 92 | Array(writeClockLimit(clock.limitSeconds.value), clock.incrementSeconds.value.toByte) ++ 93 | writeSignedInt24(legacyElapsed(clock, White).centis) ++ 94 | writeSignedInt24(legacyElapsed(clock, Black).centis) ++ 95 | clock.timer.fold(Array.empty[Byte])(writeTimer) 96 | } 97 | 98 | def read(ba: ByteArray, whiteBerserk: Boolean, blackBerserk: Boolean): Color => Clock = 99 | color => 100 | val ia = ba.value.map(toInt) 101 | 102 | // ba.size might be greater than 12 with 5 bytes timers 103 | // ba.size might be 8 if there was no timer. 104 | // #TODO remove 5 byte timer case! But fix the DB first! 105 | val timer = (ia.lengthIs == 12).so(readTimer(readInt(ia(8), ia(9), ia(10), ia(11)))) 106 | 107 | ia match 108 | case Array(b1, b2, b3, b4, b5, b6, b7, b8, _*) => 109 | val config = Clock.Config(clock.readClockLimit(b1), Clock.IncrementSeconds(b2)) 110 | val legacyWhite = Centis(readSignedInt24(b3, b4, b5)) 111 | val legacyBlack = Centis(readSignedInt24(b6, b7, b8)) 112 | val players = ByColor((whiteBerserk, legacyWhite), (blackBerserk, legacyBlack)) 113 | .map: (berserk, legacy) => 114 | ClockPlayer 115 | .withConfig(config) 116 | .copy(berserk = berserk) 117 | .setRemaining(computeRemaining(config, legacy)) 118 | Clock( 119 | config = config, 120 | color = color, 121 | players = players, 122 | timer = timer 123 | ) 124 | case _ => sys.error(s"BinaryFormat.clock.read invalid bytes: ${ba.showBytes}") 125 | 126 | private def writeTimer(timer: Timestamp) = 127 | val centis = (timer - start).centis 128 | /* 129 | * A zero timer is resolved by `readTimer` as the absence of a timer. 130 | * As a result, a clock that is started with a timer = 0 131 | * resolves as a clock that is not started. 132 | * This can happen when the clock was started at the same time as the game 133 | * For instance in simuls 134 | */ 135 | val nonZero = java.lang.Math.max(centis, 1) 136 | writeInt(nonZero) 137 | 138 | private def readTimer(l: Int) = 139 | if l != 0 then Some(start + Centis(l)) else None 140 | 141 | private def writeClockLimit(limit: Int): Byte = 142 | // The database expects a byte for a limit, and this is limit / 60. 143 | // For 0.5+0, this does not give a round number, so there needs to be 144 | // an alternative way to describe 0.5. 145 | // The max limit where limit % 60 == 0, returns 180 for limit / 60 146 | // So, for the limits where limit % 30 == 0, we can use the space 147 | // from 181-255, where 181 represents 0.25 and 182 represents 0.50... 148 | (if limit % 60 == 0 then limit / 60 else limit / 15 + 180).toByte 149 | 150 | object clock: 151 | def apply(start: java.time.Instant) = new clock(Timestamp(start.toMillis)) 152 | 153 | def readConfig(ba: ByteArray): Option[Clock.Config] = 154 | ba.value match 155 | case Array(b1, b2, _*) => Clock.Config(readClockLimit(b1), Clock.IncrementSeconds(b2)).some 156 | case _ => None 157 | 158 | def readClockLimit(i: Int) = Clock.LimitSeconds(if i < 181 then i * 60 else (i - 180) * 15) 159 | 160 | object castleLastMove: 161 | 162 | def write(clmt: CastleLastMove): ByteArray = ByteArray { 163 | 164 | val castleInt = clmt.castles.toSeq.zipWithIndex.foldLeft(0): 165 | case (acc, (false, _)) => acc 166 | case (acc, (true, p)) => acc + (1 << (3 - p)) 167 | 168 | def posInt(pos: Square): Int = (pos.file.value << 3) + pos.rank.value 169 | val lastMoveInt = clmt.lastMove.map(_.origDest).fold(0) { (o, d) => 170 | (posInt(o) << 6) + posInt(d) 171 | } 172 | Array(((castleInt << 4) + (lastMoveInt >> 8)).toByte, lastMoveInt.toByte) 173 | } 174 | 175 | def read(ba: ByteArray): CastleLastMove = 176 | val ints = ba.value.map(toInt) 177 | doRead(ints(0), ints(1)) 178 | 179 | private def doRead(b1: Int, b2: Int) = 180 | CastleLastMove( 181 | castles = Castles(b1 > 127, (b1 & 64) != 0, (b1 & 32) != 0, (b1 & 16) != 0), 182 | lastMove = for 183 | orig <- Square.at((b1 & 15) >> 1, ((b1 & 1) << 2) + (b2 >> 6)) 184 | dest <- Square.at((b2 & 63) >> 3, b2 & 7) 185 | if orig != Square.A1 || dest != Square.A1 186 | yield Uci.Move(orig, dest) 187 | ) 188 | 189 | object piece: 190 | 191 | private val groupedPos = Square.all 192 | .grouped(2) 193 | .collect { case List(p1, p2) => 194 | (p1, p2) 195 | } 196 | .toArray 197 | 198 | def write(pieces: PieceMap): ByteArray = 199 | def posInt(pos: Square): Int = 200 | (pieces 201 | .get(pos)) 202 | .fold(0): piece => 203 | piece.color.fold(0, 8) + roleToInt(piece.role) 204 | ByteArray: 205 | groupedPos.map: (p1, p2) => 206 | ((posInt(p1) << 4) + posInt(p2)).toByte 207 | 208 | def read(ba: ByteArray, variant: Variant): PieceMap = 209 | def splitInts(b: Byte) = 210 | val int = b.toInt 211 | Array(int >> 4, int & 0x0f) 212 | def intPiece(int: Int): Option[Piece] = 213 | intToRole(int & 7, variant).map: role => 214 | Piece(Color.fromWhite((int & 8) == 0), role) 215 | val pieceInts = ba.value.flatMap(splitInts) 216 | (Square.all 217 | .zip(pieceInts)) 218 | .view 219 | .flatMap: (pos, int) => 220 | intPiece(int).map(pos -> _) 221 | .to(Map) 222 | 223 | // cache standard start position 224 | val standard = write(Position.init(chess.variant.Standard, White).pieces) 225 | 226 | private def intToRole(int: Int, variant: Variant): Option[Role] = 227 | int match 228 | case 6 => Some(Pawn) 229 | case 1 => Some(King) 230 | case 2 => Some(Queen) 231 | case 3 => Some(Rook) 232 | case 4 => Some(Knight) 233 | case 5 => Some(Bishop) 234 | // Legacy from when we used to have an 'Antiking' piece 235 | case 7 if variant.antichess => Some(King) 236 | case _ => None 237 | private def roleToInt(role: Role): Int = 238 | role match 239 | case Pawn => 6 240 | case King => 1 241 | case Queen => 2 242 | case Rook => 3 243 | case Knight => 4 244 | case Bishop => 5 245 | 246 | object unmovedRooks: 247 | 248 | val emptyByteArray = ByteArray(Array(0, 0)) 249 | 250 | def write(o: UnmovedRooks): ByteArray = 251 | if o.isEmpty then emptyByteArray 252 | else 253 | ByteArray: 254 | var white = 0 255 | var black = 0 256 | o.toList.foreach: pos => 257 | if pos.rank == Rank.First then white = white | (1 << (7 - pos.file.value)) 258 | else black = black | (1 << (7 - pos.file.value)) 259 | Array(white.toByte, black.toByte) 260 | 261 | private def bitAt(n: Int, k: Int) = (n >> k) & 1 262 | 263 | private val arrIndexes = 0 to 1 264 | private val bitIndexes = 0 to 7 265 | private val whiteStd = Set(Square.A1, Square.H1) 266 | private val blackStd = Set(Square.A8, Square.H8) 267 | 268 | def read(ba: ByteArray) = 269 | var set = Set.empty[Square] 270 | arrIndexes.foreach: i => 271 | val int = ba.value(i).toInt 272 | if int != 0 then 273 | if int == -127 then set = if i == 0 then whiteStd else set ++ blackStd 274 | else 275 | bitIndexes.foreach: j => 276 | if bitAt(int, j) == 1 then set = set + Square.at(7 - j, 7 * i).get 277 | UnmovedRooks(set) 278 | 279 | @inline private def toInt(b: Byte): Int = b & 0xff 280 | 281 | def writeInt24(int: Int) = 282 | val i = if int < (1 << 24) then int else 0 283 | Array((i >>> 16).toByte, (i >>> 8).toByte, i.toByte) 284 | 285 | private val int23Max = 1 << 23 286 | def writeSignedInt24(int: Int) = 287 | val i = if int < 0 then int23Max - int else math.min(int, int23Max) 288 | writeInt24(i) 289 | 290 | def readInt24(b1: Int, b2: Int, b3: Int) = (b1 << 16) | (b2 << 8) | b3 291 | 292 | def readSignedInt24(b1: Int, b2: Int, b3: Int) = 293 | val i = readInt24(b1, b2, b3) 294 | if i > int23Max then int23Max - i else i 295 | 296 | def writeInt(i: Int) = 297 | Array( 298 | (i >>> 24).toByte, 299 | (i >>> 16).toByte, 300 | (i >>> 8).toByte, 301 | i.toByte 302 | ) 303 | 304 | def readInt(b1: Int, b2: Int, b3: Int, b4: Int) = 305 | (b1 << 24) | (b2 << 16) | (b3 << 8) | b4 306 | -------------------------------------------------------------------------------- /src/main/scala/lila/game/Game.scala: -------------------------------------------------------------------------------- 1 | package lila 2 | package game 3 | 4 | import scalalib.extensions.* 5 | import scala.language.postfixOps 6 | 7 | import chess.Color.{ Black, White } 8 | import chess.format.{ Fen, Uci } 9 | import chess.opening.{ Opening, OpeningDb } 10 | import chess.variant.Variant 11 | import chess.format.pgn.SanStr 12 | import chess.{ ByColor, Castles, Centis, CheckCount, Clock, Color, Game as ChessGame, Mode, Status } 13 | 14 | import lila.common.Sequence 15 | import lila.db.ByteArray 16 | 17 | case class Game( 18 | id: String, 19 | players: ByColor[Player], 20 | chess: ChessGame, 21 | status: Status, 22 | daysPerTurn: Option[Int], 23 | binaryMoveTimes: Option[ByteArray] = None, 24 | clockHistory: Option[ClockHistory] = Option(ClockHistory()), 25 | mode: Mode = Mode.default, 26 | createdAt: Instant, 27 | movedAt: Instant, 28 | metadata: Metadata 29 | ): 30 | export chess.{ clock, player as turnColor, ply, sans, board, startedAtPly } 31 | export chess.board.{ history, variant } 32 | 33 | def player: Player = players(turnColor) 34 | 35 | def playerByUserId(userId: String): Option[Player] = 36 | players.find(_.userId contains userId) 37 | def opponentByUserId(userId: String): Option[Player] = 38 | playerByUserId(userId).map(opponent) 39 | 40 | def opponent(p: Player): Player = opponent(p.color) 41 | 42 | def opponent(c: Color): Player = players(!c) 43 | 44 | def turnOf(p: Player): Boolean = p == player 45 | def turnOf(c: Color): Boolean = c == turnColor 46 | 47 | def playedTurns = ply - startedAtPly 48 | 49 | def flagged = if status == Status.Outoftime then Some(turnColor) else None 50 | 51 | export metadata.* 52 | 53 | def isTournament = tournamentId.isDefined 54 | def isSimul = simulId.isDefined 55 | def isMandatory = isTournament || isSimul 56 | def nonMandatory = !isMandatory 57 | 58 | def hasChat = !isTournament && !isSimul && nonAi 59 | 60 | def everyOther[A](l: List[A]): List[A] = l match 61 | case a :: _ :: tail => a :: everyOther(tail) 62 | case _ => l 63 | 64 | def moveTimes(color: Color): Option[List[Centis]] = { 65 | for 66 | clk <- clock 67 | inc = clk.incrementOf(color) 68 | history <- clockHistory 69 | clocks = history(color) 70 | yield Centis(0) :: { 71 | val pairs = clocks.iterator.zip(clocks.iterator.drop(1)) 72 | 73 | // We need to determine if this color's last clock had inc applied. 74 | // if finished and history.size == playedTurns then game was ended 75 | // by a players move, such as with mate or autodraw. In this case, 76 | // the last move of the game, and the only one without inc, is the 77 | // last entry of the clock history for !turnColor. 78 | // 79 | // On the other hand, if history.size is more than playedTurns, 80 | // then the game ended during a players turn by async event, and 81 | // the last recorded time is in the history for turnColor. 82 | val noLastInc = finished && (history.size <= playedTurns.value) == (color != turnColor) 83 | 84 | pairs.map { case (first, second) => 85 | { 86 | val d = first - second 87 | if pairs.hasNext || !noLastInc then d + inc else d 88 | }.nonNeg 89 | }.toList 90 | } 91 | }.orElse(binaryMoveTimes.map { binary => 92 | // TODO: make movetime.read return List after writes are disabled. 93 | val base = BinaryFormat.moveTime.read(binary, playedTurns) 94 | val mts = if color == startColor then base else base.drop(1) 95 | everyOther(mts.toList) 96 | }) 97 | 98 | def moveTimes: Option[Vector[Centis]] = 99 | for 100 | a <- moveTimes(startColor) 101 | b <- moveTimes(!startColor) 102 | yield Sequence.interleave(a, b) 103 | 104 | def bothClockStates: Option[Vector[Centis]] = 105 | clockHistory.map(_.bothClockStates(startColor)) 106 | 107 | def sansOf(color: Color): Vector[SanStr] = 108 | val pivot = if color == startColor then 0 else 1 109 | sans.zipWithIndex.collect: 110 | case (e, i) if (i % 2) == pivot => e 111 | 112 | def lastMoveKeys: Option[String] = history.lastMove.map { 113 | case Uci.Drop(target, _) => s"$target$target" 114 | case m: Uci.Move => m.keys 115 | } 116 | 117 | def correspondenceClock: Option[CorrespondenceClock] = 118 | daysPerTurn.map: days => 119 | val increment = days * 24 * 60 * 60 120 | val secondsLeft = (movedAt.toSeconds + increment - nowSeconds).toInt.max(0) 121 | CorrespondenceClock( 122 | increment = increment, 123 | whiteTime = turnColor.fold(secondsLeft, increment).toFloat, 124 | blackTime = turnColor.fold(increment, secondsLeft).toFloat 125 | ) 126 | 127 | def started = status >= Status.Started 128 | 129 | def notStarted = !started 130 | 131 | def aborted = status == Status.Aborted 132 | 133 | def playedThenAborted = aborted && bothPlayersHaveMoved 134 | 135 | def playable = status < Status.Aborted && !imported 136 | 137 | def playableEvenImported = status < Status.Aborted 138 | 139 | def alarmable = hasCorrespondenceClock && playable && nonAi 140 | 141 | def continuable = status != Status.Mate && status != Status.Stalemate 142 | 143 | def aiLevel: Option[Int] = players.find(_.isAi).flatMap(_.aiLevel) 144 | 145 | def hasAi: Boolean = players.exists(_.isAi) 146 | def nonAi = !hasAi 147 | 148 | def aiPov: Option[Pov] = players.find(_.isAi).map(_.color).map(pov) 149 | 150 | def boosted = rated && finished && bothPlayersHaveMoved && playedTurns < 10 151 | 152 | def rated = mode.rated 153 | def casual = !rated 154 | 155 | def finished = status >= Status.Mate 156 | 157 | def finishedOrAborted = finished || aborted 158 | 159 | def accountable = playedTurns >= 2 || isTournament 160 | 161 | def imported = source contains Source.Import 162 | 163 | def fromPool = source contains Source.Pool 164 | def fromLobby = source contains Source.Lobby 165 | def fromFriend = source contains Source.Friend 166 | 167 | def winner = players.find(_.wins) 168 | 169 | def loser = winner.map(opponent) 170 | 171 | def winnerColor: Option[Color] = winner.map(_.color) 172 | 173 | def winnerUserId: Option[String] = winner.flatMap(_.userId) 174 | 175 | def loserUserId: Option[String] = loser.flatMap(_.userId) 176 | 177 | def wonBy(c: Color): Option[Boolean] = winnerColor.map(_ == c) 178 | 179 | def lostBy(c: Color): Option[Boolean] = winnerColor.map(_ != c) 180 | 181 | def drawn = finished && winner.isEmpty 182 | 183 | def hasClock = clock.isDefined 184 | 185 | def hasCorrespondenceClock = daysPerTurn.isDefined 186 | 187 | def isUnlimited = !hasClock && !hasCorrespondenceClock 188 | 189 | def estimateClockTotalTime = clock.map(_.estimateTotalSeconds) 190 | 191 | def estimateTotalTime = 192 | estimateClockTotalTime.orElse(correspondenceClock.map(_.estimateTotalTime)).getOrElse(1200) 193 | 194 | def onePlayerHasMoved = playedTurns > 0 195 | def bothPlayersHaveMoved = playedTurns > 1 196 | 197 | def startColor = startedAtPly.turn 198 | 199 | def ratingVariant = 200 | if isTournament && board.variant.fromPosition then _root_.chess.variant.Standard 201 | else variant 202 | 203 | def fromPosition = variant.fromPosition || source.contains(Source.Position) 204 | 205 | lazy val opening: Option[Opening.AtPly] = 206 | (!fromPosition && Variant.list.openingSensibleVariants(variant)).so(OpeningDb.search(sans)) 207 | 208 | def synthetic = id == Game.syntheticId 209 | 210 | def pov(c: Color) = Pov(this, c) 211 | def whitePov = pov(White) 212 | def blackPov = pov(Black) 213 | def playerPov(p: Player) = pov(p.color) 214 | def loserPov = loser.map(playerPov) 215 | 216 | def speed = _root_.chess.Speed(clock.map(_.config)) 217 | 218 | def perfKey = PerfPicker.key(this) 219 | def perfType = lila.rating.PerfType(perfKey) 220 | 221 | object Game: 222 | 223 | type ID = String 224 | 225 | case class WithInitialFen(game: Game, fen: Option[Fen.Full]) 226 | 227 | val syntheticId = "synthetic" 228 | 229 | val maxPlayingRealtime = 100 // plus 200 correspondence games 230 | 231 | val analysableVariants: Set[Variant] = Set( 232 | chess.variant.Standard, 233 | chess.variant.Crazyhouse, 234 | chess.variant.Chess960, 235 | chess.variant.KingOfTheHill, 236 | chess.variant.ThreeCheck, 237 | chess.variant.Antichess, 238 | chess.variant.FromPosition, 239 | chess.variant.Horde, 240 | chess.variant.Atomic, 241 | chess.variant.RacingKings 242 | ) 243 | 244 | val variantsWhereWhiteIsBetter: Set[Variant] = Set( 245 | chess.variant.ThreeCheck, 246 | chess.variant.Atomic, 247 | chess.variant.Horde, 248 | chess.variant.RacingKings, 249 | chess.variant.Antichess 250 | ) 251 | 252 | val visualisableVariants: Set[Variant] = Set( 253 | chess.variant.Standard, 254 | chess.variant.Chess960 255 | ) 256 | 257 | val hordeWhitePawnsSince = instantOf(2015, 4, 11, 10, 0) 258 | 259 | def isOldHorde(game: Game) = 260 | game.variant == chess.variant.Horde && 261 | game.createdAt.isBefore(Game.hordeWhitePawnsSince) 262 | 263 | def allowRated(variant: Variant, clock: Clock.Config) = 264 | variant.standard || clock.estimateTotalTime >= Centis(3000) 265 | 266 | val gameIdSize = 8 267 | val playerIdSize = 4 268 | val fullIdSize = 12 269 | val tokenSize = 4 270 | 271 | def takeGameId(fullId: String) = fullId.take(gameIdSize) 272 | def takePlayerId(fullId: String) = fullId.drop(gameIdSize) 273 | 274 | private[game] val emptyCheckCount = CheckCount(0, 0) 275 | 276 | object BSONFields: 277 | 278 | val id = "_id" 279 | val whitePlayer = "p0" 280 | val blackPlayer = "p1" 281 | val playerIds = "is" 282 | val playerUids = "us" 283 | val playingUids = "pl" 284 | val binaryPieces = "ps" 285 | val oldPgn = "pg" 286 | val huffmanPgn = "hp" 287 | val status = "s" 288 | val turns = "t" 289 | val startedAtTurn = "st" 290 | val clock = "c" 291 | val positionHashes = "ph" 292 | val checkCount = "cc" 293 | val castleLastMove = "cl" 294 | val unmovedRooks = "ur" 295 | val daysPerTurn = "cd" 296 | val moveTimes = "mt" 297 | val whiteClockHistory = "cw" 298 | val blackClockHistory = "cb" 299 | val rated = "ra" 300 | val analysed = "an" 301 | val variant = "v" 302 | val crazyData = "chd" 303 | val next = "ne" 304 | val bookmarks = "bm" 305 | val createdAt = "ca" 306 | val movedAt = "ua" // ua = updatedAt (bc) 307 | val source = "so" 308 | val pgnImport = "pgni" 309 | val tournamentId = "tid" 310 | val simulId = "sid" 311 | val swissId = "iid" 312 | val tvAt = "tv" 313 | val winnerColor = "w" 314 | val winnerId = "wid" 315 | val initialFen = "if" 316 | val checkAt = "ck" 317 | 318 | case class CastleLastMove(castles: Castles, lastMove: Option[Uci]) 319 | 320 | object CastleLastMove: 321 | 322 | def init = CastleLastMove(Castles.init, None) 323 | 324 | import reactivemongo.api.bson.* 325 | import lila.db.dsl.* 326 | import lila.db.ByteArray.byteArrayHandler 327 | 328 | private[game] given castleLastMoveHandler: BSONHandler[CastleLastMove] = tryHandler[CastleLastMove]( 329 | { case bin: BSONBinary => 330 | byteArrayHandler.readTry(bin).map(BinaryFormat.castleLastMove.read) 331 | }, 332 | clmt => byteArrayHandler.writeTry { BinaryFormat.castleLastMove.write(clmt) }.get 333 | ) 334 | 335 | case class ClockHistory( 336 | white: Vector[Centis] = Vector.empty, 337 | black: Vector[Centis] = Vector.empty 338 | ): 339 | 340 | def update(color: Color, f: Vector[Centis] => Vector[Centis]): ClockHistory = 341 | color.fold(copy(white = f(white)), copy(black = f(black))) 342 | 343 | def record(color: Color, clock: Clock): ClockHistory = 344 | update(color, _ :+ clock.remainingTime(color)) 345 | 346 | def reset(color: Color) = update(color, _ => Vector.empty) 347 | 348 | def apply(color: Color): Vector[Centis] = color.fold(white, black) 349 | 350 | def last(color: Color) = apply(color).lastOption 351 | 352 | def size = white.size + black.size 353 | 354 | // first state is of the color that moved first. 355 | def bothClockStates(firstMoveBy: Color): Vector[Centis] = 356 | Sequence.interleave( 357 | firstMoveBy.fold(white, black), 358 | firstMoveBy.fold(black, white) 359 | ) 360 | -------------------------------------------------------------------------------- /src/main/scala/lila/game/LightGame.scala: -------------------------------------------------------------------------------- 1 | package lila 2 | package game 3 | 4 | import chess.{ Color, Status } 5 | 6 | case class LightGame( 7 | id: String, 8 | whitePlayer: LightPlayer, 9 | blackPlayer: LightPlayer, 10 | status: Status, 11 | win: Option[Color] 12 | ): 13 | def playable = status < Status.Aborted 14 | def player(color: Color): LightPlayer = color.fold(whitePlayer, blackPlayer) 15 | def players = List(whitePlayer, blackPlayer) 16 | def playerByUserId(userId: String): Option[LightPlayer] = players.find(_.userId contains userId) 17 | def finished = status >= Status.Mate 18 | 19 | object LightGame: 20 | 21 | import Game.BSONFields as F 22 | 23 | def projection = 24 | lila.db.dsl.$doc( 25 | F.whitePlayer -> true, 26 | F.blackPlayer -> true, 27 | F.playerUids -> true, 28 | F.winnerColor -> true, 29 | F.status -> true 30 | ) 31 | 32 | case class LightPlayer( 33 | color: Color, 34 | aiLevel: Option[Int], 35 | userId: Option[String] = None, 36 | rating: Option[Int] = None, 37 | ratingDiff: Option[Int] = None, 38 | provisional: Boolean = false, 39 | berserk: Boolean = false 40 | ) 41 | 42 | object LightPlayer: 43 | 44 | import reactivemongo.api.bson.* 45 | import lila.db.dsl.* 46 | 47 | private[game] type Builder = Color => Option[String] => LightPlayer 48 | 49 | private def safeRange[A](range: Range)(a: A): Option[A] = range.contains(a).option(a) 50 | private val ratingRange = safeRange[Int](0 to 4000) 51 | private val ratingDiffRange = safeRange[Int](-1000 to 1000) 52 | 53 | given lightPlayerReader: BSONDocumentReader[Builder]: 54 | import scala.util.{ Success, Try } 55 | def readDocument(doc: Bdoc): Try[Builder] = Success(builderRead(doc)) 56 | 57 | def builderRead(doc: Bdoc): Builder = color => 58 | userId => 59 | import Player.BSONFields.* 60 | LightPlayer( 61 | color = color, 62 | aiLevel = doc.int(aiLevel), 63 | userId = userId, 64 | rating = doc.getAsOpt[Int](rating).flatMap(ratingRange), 65 | ratingDiff = doc.getAsOpt[Int](ratingDiff).flatMap(ratingDiffRange), 66 | provisional = ~doc.getAsOpt[Boolean](provisional), 67 | berserk = doc.booleanLike(berserk).getOrElse(false) 68 | ) 69 | -------------------------------------------------------------------------------- /src/main/scala/lila/game/Metadata.scala: -------------------------------------------------------------------------------- 1 | package lila.game 2 | 3 | private[game] case class Metadata( 4 | source: Option[Source], 5 | tournamentId: Option[String], 6 | swissId: Option[String], 7 | simulId: Option[String], 8 | analysed: Boolean 9 | ) 10 | -------------------------------------------------------------------------------- /src/main/scala/lila/game/PerfPicker.scala: -------------------------------------------------------------------------------- 1 | package lila.game 2 | 3 | import chess.Speed 4 | import lila.rating.PerfType 5 | 6 | object PerfPicker: 7 | 8 | def key(speed: Speed, variant: chess.variant.Variant, daysPerTurn: Option[Int]): String = 9 | if variant.standard then 10 | if daysPerTurn.isDefined || speed == Speed.Correspondence then PerfType.Correspondence.key 11 | else speed.key.value 12 | else variant.key.value 13 | 14 | def key(game: Game): String = key(game.speed, game.ratingVariant, game.daysPerTurn) 15 | -------------------------------------------------------------------------------- /src/main/scala/lila/game/PgnDump.scala: -------------------------------------------------------------------------------- 1 | package lila 2 | package game 3 | 4 | import chess.format.pgn.{ InitialComments, Pgn, PgnTree, SanStr, Tag, TagType, Tags } 5 | import chess.format.Fen 6 | import chess.format.pgn as chessPgn 7 | import chess.{ Centis, Color, Ply } 8 | import lichess.Users 9 | 10 | // #TODO add draw offers comments 11 | object PgnDump: 12 | 13 | def apply(game: Game, users: Users, initialFen: Option[Fen.Full]): Pgn = 14 | val ts = tags(game, users, initialFen) 15 | val fenSituation = ts.fen.flatMap(Fen.readWithMoveNumber) 16 | val initialPly = fenSituation.fold(Ply.initial)(_.ply) 17 | val tree = makeTree( 18 | game.sans, 19 | ~game.bothClockStates, 20 | game.startColor 21 | ) 22 | Pgn(ts, InitialComments.empty, tree, initialPly.next) 23 | 24 | def result(game: Game) = 25 | if game.finished then game.winnerColor.fold("1/2-1/2")(_.fold("1-0", "0-1")) 26 | else "*" 27 | 28 | private def gameUrl(id: String) = s"https://lichess.org/$id" 29 | 30 | private def elo(p: Player) = p.rating.fold("?")(_.toString) 31 | 32 | private def player(g: Game, color: Color, users: Users) = 33 | val player = g.players(color) 34 | player.aiLevel.fold(users(color).name)("lichess AI level " + _) 35 | 36 | private def eventOf(game: Game) = 37 | val perf = game.perfType.fold("Standard")(_.name) 38 | game.tournamentId 39 | .map { id => 40 | s"${game.mode} $perf tournament https://lichess.org/tournament/$id" 41 | } 42 | .orElse(game.simulId.map { id => 43 | s"$perf simul https://lichess.org/simul/$id" 44 | }) 45 | .orElse(game.swissId.map { id => 46 | s"$perf swiss https://lichess.org/swiss/$id" 47 | }) 48 | .getOrElse { 49 | s"${game.mode} $perf game" 50 | } 51 | 52 | private def ratingDiffTag(p: Player, tag: Tag.type => TagType) = 53 | p.ratingDiff.map { rd => 54 | Tag(tag(Tag), s"${if rd >= 0 then "+" else ""}$rd") 55 | } 56 | 57 | private val emptyRound = Tag(_.Round, "-") 58 | 59 | def tags(game: Game, users: Users, initialFen: Option[Fen.Full]): Tags = Tags: 60 | val date = Tag.UTCDate.format.print(game.createdAt) 61 | List( 62 | Tag(_.Event, eventOf(game)), 63 | Tag(_.Site, gameUrl(game.id)), 64 | Tag(_.Date, date), 65 | emptyRound, 66 | Tag(_.White, player(game, chess.White, users)), 67 | Tag(_.Black, player(game, chess.Black, users)), 68 | Tag(_.Result, result(game)), 69 | Tag(_.UTCDate, Tag.UTCDate.format.print(game.createdAt)), 70 | Tag(_.UTCTime, Tag.UTCTime.format.print(game.createdAt)), 71 | Tag(_.WhiteElo, elo(game.players.white)), 72 | Tag(_.BlackElo, elo(game.players.black)) 73 | ) ::: List( 74 | ratingDiffTag(game.players.white, _.WhiteRatingDiff), 75 | ratingDiffTag(game.players.black, _.BlackRatingDiff), 76 | users.white.title.map { t => 77 | Tag(_.WhiteTitle, t) 78 | }, 79 | users.black.title.map { t => 80 | Tag(_.BlackTitle, t) 81 | }, 82 | if game.variant.standard then Some(Tag(_.ECO, game.opening.fold("?")(_.opening.eco))) else None, 83 | if game.variant.standard then Some(Tag(_.Opening, game.opening.fold("?")(_.opening.name))) else None, 84 | Some( 85 | Tag( 86 | _.TimeControl, 87 | game.clock.fold("-") { c => 88 | s"${c.limit.roundSeconds}+${c.increment.roundSeconds}" 89 | } 90 | ) 91 | ), 92 | Some( 93 | Tag( 94 | _.Termination, { 95 | import chess.Status.* 96 | game.status match 97 | case Created | Started => "Unterminated" 98 | case Aborted | NoStart => "Abandoned" 99 | case Timeout | Outoftime => "Time forfeit" 100 | case Resign | Draw | Stalemate | Mate | VariantEnd => "Normal" 101 | case Cheat => "Rules infraction" 102 | case UnknownFinish => "Unknown" 103 | } 104 | ) 105 | ), 106 | if !game.variant.standardInitialPosition then 107 | Some(Tag(_.FEN, initialFen.getOrElse(game.variant.initialFen))) 108 | else None, 109 | if !game.variant.standardInitialPosition then Some(Tag("SetUp", "1")) else None, 110 | if game.variant.exotic then Some(Tag(_.Variant, game.variant.name)) else None 111 | ).flatten 112 | 113 | def makeTree( 114 | moves: Seq[SanStr], 115 | clocks: Vector[Centis], 116 | startColor: Color 117 | ): Option[PgnTree] = 118 | val clockOffset = startColor.fold(0, 1) 119 | def f(san: SanStr, index: Int) = chessPgn.Move( 120 | san = san, 121 | timeLeft = clocks.lift(index - clockOffset).map(_.roundSeconds) 122 | ) 123 | chess.Tree.buildWithIndex(moves, f) 124 | -------------------------------------------------------------------------------- /src/main/scala/lila/game/PgnStorage.scala: -------------------------------------------------------------------------------- 1 | package lila.game 2 | 3 | import chess.* 4 | import chess.format.Uci 5 | import chess.format.pgn.SanStr 6 | 7 | import lila.db.ByteArray 8 | 9 | sealed trait PgnStorage 10 | 11 | private object PgnStorage: 12 | 13 | case object OldBin: 14 | 15 | def encode(sans: Vector[SanStr]) = 16 | ByteArray: 17 | format.pgn.Binary.writeMoves(sans).get 18 | 19 | def decode(bytes: ByteArray, plies: Ply): Vector[SanStr] = 20 | format.pgn.Binary.readMoves(bytes.value.toList, plies.value).get.toVector 21 | 22 | case object Huffman: 23 | 24 | import org.lichess.compression.game.{ Board as JavaBoard, Encoder } 25 | 26 | def encode(sans: Vector[SanStr]) = 27 | ByteArray: 28 | Encoder.encode(SanStr.raw(sans.toArray)) 29 | 30 | def decode(bytes: ByteArray, plies: Ply, id: String): Decoded = 31 | val decoded = 32 | try Encoder.decode(bytes.value, plies.value) 33 | catch 34 | case e: java.nio.BufferUnderflowException => 35 | println(s"Can't decode game $id PGN") 36 | throw e 37 | Decoded( 38 | sans = SanStr.from(decoded.pgnMoves.toVector), 39 | board = chessBoard(decoded.board), 40 | positionHashes = PositionHash(decoded.positionHashes), 41 | unmovedRooks = UnmovedRooks(decoded.board.castlingRights), 42 | lastMove = Option(decoded.lastUci).flatMap(Uci.apply), 43 | castles = Castles(decoded.board.castlingRights), 44 | halfMoveClock = HalfMoveClock(decoded.halfMoveClock) 45 | ) 46 | 47 | private def chessBoard(b: JavaBoard): Board = 48 | Board( 49 | occupied = Bitboard(b.occupied), 50 | white = Bitboard(b.white), 51 | black = Bitboard(b.black), 52 | pawns = Bitboard(b.pawns), 53 | knights = Bitboard(b.knights), 54 | bishops = Bitboard(b.bishops), 55 | rooks = Bitboard(b.rooks), 56 | queens = Bitboard(b.queens), 57 | kings = Bitboard(b.kings) 58 | ) 59 | 60 | case class Decoded( 61 | sans: Vector[SanStr], 62 | board: Board, 63 | positionHashes: PositionHash, // irrelevant after game ends 64 | unmovedRooks: UnmovedRooks, // irrelevant after game ends 65 | lastMove: Option[Uci], 66 | castles: Castles, // irrelevant after game ends 67 | halfMoveClock: HalfMoveClock // irrelevant after game ends 68 | ) 69 | -------------------------------------------------------------------------------- /src/main/scala/lila/game/Player.scala: -------------------------------------------------------------------------------- 1 | package lila.game 2 | 3 | import chess.Color 4 | 5 | case class PlayerUser(id: String, rating: Int, ratingDiff: Option[Int]) 6 | 7 | case class Player( 8 | id: String, 9 | color: Color, 10 | aiLevel: Option[Int], 11 | isWinner: Option[Boolean] = None, 12 | userId: Option[String] = None, 13 | rating: Option[Int] = None, 14 | ratingDiff: Option[Int] = None, 15 | provisional: Boolean = false, 16 | berserk: Boolean = false, 17 | name: Option[String] = None 18 | ): 19 | 20 | def playerUser = userId.flatMap { uid => 21 | rating.map { PlayerUser(uid, _, ratingDiff) } 22 | } 23 | 24 | def isAi = aiLevel.isDefined 25 | 26 | def isHuman = !isAi 27 | 28 | def hasUser = userId.isDefined 29 | 30 | def wins = isWinner.getOrElse(false) 31 | 32 | def ratingAfter = rating.map(_ + ratingDiff.getOrElse(0)) 33 | 34 | object Player: 35 | 36 | object BSONFields: 37 | 38 | val aiLevel = "ai" 39 | val isOfferingDraw = "od" 40 | val isOfferingRematch = "or" 41 | val lastDrawOffer = "ld" 42 | val proposeTakebackAt = "ta" 43 | val rating = "e" 44 | val ratingDiff = "d" 45 | val provisional = "p" 46 | val berserk = "be" 47 | val name = "na" 48 | 49 | type Id = String 50 | type UserId = Option[String] 51 | type Win = Option[Boolean] 52 | type Builder = Color => Id => UserId => Win => Player 53 | 54 | def from(light: LightGame, color: Color, ids: String, doc: lila.db.dsl.Bdoc): Player = 55 | import BSONFields.* 56 | val p = light.player(color) 57 | Player( 58 | id = color.fold(ids.take(4), ids.drop(4)), 59 | color = p.color, 60 | aiLevel = p.aiLevel, 61 | isWinner = light.win.map(_ == color), 62 | userId = p.userId, 63 | rating = p.rating, 64 | ratingDiff = p.ratingDiff, 65 | provisional = p.provisional, 66 | berserk = p.berserk, 67 | name = doc.string(name) 68 | ) 69 | -------------------------------------------------------------------------------- /src/main/scala/lila/game/Pov.scala: -------------------------------------------------------------------------------- 1 | package lila.game 2 | 3 | import chess.Color 4 | 5 | case class Pov(game: Game, color: Color): 6 | 7 | def player = game.players(color) 8 | 9 | def playerId = player.id 10 | 11 | def gameId = game.id 12 | 13 | def opponent = game.players(!color) 14 | 15 | def unary_! = Pov(game, !color) 16 | 17 | def ref = PovRef(game.id, color) 18 | 19 | def withGame(g: Game) = copy(game = g) 20 | def withColor(c: Color) = copy(color = c) 21 | 22 | lazy val isMyTurn = game.started && game.playable && game.turnColor == color 23 | 24 | def win = game.wonBy(color) 25 | 26 | def loss = game.lostBy(color) 27 | 28 | override def toString = ref.toString 29 | 30 | object Pov: 31 | 32 | def player(game: Game) = apply(game, game.player) 33 | 34 | def apply(game: Game, player: Player) = new Pov(game, player.color) 35 | 36 | def ofUserId(game: Game, userId: String): Option[Pov] = 37 | game.playerByUserId(userId).map { apply(game, _) } 38 | 39 | def opponentOfUserId(game: Game, userId: String): Option[Player] = 40 | ofUserId(game, userId).map(_.opponent) 41 | 42 | case class PovRef(gameId: String, color: Color): 43 | 44 | def unary_! = PovRef(gameId, !color) 45 | 46 | override def toString = s"$gameId/${color.name}" 47 | 48 | case class PlayerRef(gameId: String, playerId: String) 49 | 50 | object PlayerRef: 51 | 52 | def apply(fullId: String): PlayerRef = PlayerRef(Game.takeGameId(fullId), Game.takePlayerId(fullId)) 53 | -------------------------------------------------------------------------------- /src/main/scala/lila/game/Source.scala: -------------------------------------------------------------------------------- 1 | package lila.game 2 | 3 | sealed abstract class Source(val id: Int): 4 | 5 | lazy val name = toString.toLowerCase 6 | 7 | object Source: 8 | 9 | case object Lobby extends Source(id = 1) 10 | case object Friend extends Source(id = 2) 11 | case object Ai extends Source(id = 3) 12 | case object Api extends Source(id = 4) 13 | case object Tournament extends Source(id = 5) 14 | case object Position extends Source(id = 6) 15 | case object Import extends Source(id = 7) 16 | case object ImportLive extends Source(id = 9) 17 | case object Simul extends Source(id = 10) 18 | case object Relay extends Source(id = 11) 19 | case object Pool extends Source(id = 12) 20 | 21 | val all = List( 22 | Lobby, 23 | Friend, 24 | Ai, 25 | Api, 26 | Tournament, 27 | Position, 28 | Import, 29 | Simul, 30 | Relay, 31 | Pool 32 | ) 33 | val byId = all.view.map { v => 34 | (v.id, v) 35 | }.toMap 36 | 37 | val searchable = 38 | List(Lobby, Friend, Ai, Position, Import, Tournament, Simul, Pool) 39 | 40 | def apply(id: Int): Option[Source] = byId.get(id) 41 | -------------------------------------------------------------------------------- /src/main/scala/lila/package.scala: -------------------------------------------------------------------------------- 1 | package lila 2 | 3 | export scalalib.newtypes.{ *, given } 4 | export scalalib.zeros.given 5 | export scalalib.extensions.{ *, given } 6 | export scalalib.time.* 7 | 8 | export cats.syntax.all.* 9 | export cats.{ Eq, Show } 10 | export cats.data.NonEmptyList 11 | export java.time.{ Instant, LocalDateTime } 12 | 13 | import scala.util.Try 14 | implicit final class LilaPimpedTryList[A](list: List[Try[A]]): 15 | def sequence: Try[List[A]] = 16 | (Try(List[A]()) /: list) { (a, b) => 17 | a.flatMap(c => b.map(d => d :: c)) 18 | }.map(_.reverse) 19 | implicit final class LilaPimpedOptionList[A](list: List[Option[A]]): 20 | def sequence: Option[List[A]] = 21 | (Option(List[A]()) /: list) { (a, b) => 22 | a.flatMap(c => b.map(d => d :: c)) 23 | }.map(_.reverse) 24 | -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | index.html 3 | -------------------------------------------------------------------------------- /web/broadcast-table.html.tpl: -------------------------------------------------------------------------------- 1 |

2 | games in PGN format, from the official Lichess broadcasts. 3 |

4 |

5 | Broadcast games are released under the 6 | Creative Commons Attribution-ShareAlike 4.0 license.
7 |

8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
MonthSizeGamesViewDownload
24 | 25 |

26 | Here's a plain text download list, 27 | and the SHA256 checksums. 28 |

29 | -------------------------------------------------------------------------------- /web/chess-social-networks-paper.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lichess-org/database/e6eb7e10bded5ae6a723029a29c29b24cd6e311f/web/chess-social-networks-paper.pdf -------------------------------------------------------------------------------- /web/index.html.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | lichess.org open database 10 | 11 | 12 | 13 |
14 |
15 |
16 | Lichess logo 17 |

18 | 19 | lichess.org 20 | 21 | open database 22 |

23 |
24 |

25 | Database exports are released under the 26 | Creative Commons CC0 license.
27 | Use them for research, commercial purpose, publication, anything you like.
28 | You can download, modify and redistribute them, without asking for permission.
29 |

30 |
31 |
32 |
33 | 40 |

41 | chess positions evaluated with Stockfish. 42 |
43 | Produced by, and for, the Lichess analysis board, 44 | running various flavours of Stockfish within user browsers. 45 |

46 | Download lichess_db_eval.jsonl.zst 47 |

48 |

49 | This file was last updated on . 50 |

51 |

Format

52 |

Evaluations are formatted as JSON; one position per line.

53 |

The schema of a position looks like this:

54 |
 55 | {
 56 |   "fen":          // the position FEN only contains pieces, active color, castling rights, and en passant square.
 57 |   "evals": [      // a list of evaluations, ordered by number of PVs.
 58 |       "knodes":   // number of kilo-nodes searched by the engine
 59 |       "depth":    // depth reached by the engine
 60 |       "pvs": [    // list of principal variations
 61 |         "cp":     // centipawn evaluation. Omitted if mate is certain.
 62 |         "mate":   // mate evaluation. Omitted if mate is not certain.
 63 |         "line":   // principal variation, in UCI format.
 64 | }
65 |

Each position can have multiple evaluations, each with a different number of PVs. 66 |

Sample

67 |
 68 | {
 69 |   "fen": "2bq1rk1/pr3ppn/1p2p3/7P/2pP1B1P/2P5/PPQ2PB1/R3R1K1 w - -",
 70 |   "evals": [
 71 |     {
 72 |       "pvs": [
 73 |         {
 74 |           "cp": 311,
 75 |           "line": "g2e4 f7f5 e4b7 c8b7 f2f3 b7f3 e1e6 d8h4 c2h2 h4g4"
 76 |         }
 77 |       ],
 78 |       "knodes": 206765,
 79 |       "depth": 36
 80 |     },
 81 |     {
 82 |       "pvs": [
 83 |         {
 84 |           "cp": 292,
 85 |           "line": "g2e4 f7f5 e4b7 c8b7 f2f3 b7f3 e1e6 d8h4 c2h2 h4g4"
 86 |         },
 87 |         {
 88 |           "cp": 277,
 89 |           "line": "f4g3 f7f5 e1e5 d8f6 a1e1 b7f7 g2c6 f8d8 d4d5 e6d5"
 90 |         }
 91 |       ],
 92 |       "knodes": 92958,
 93 |       "depth": 34
 94 |     },
 95 |     {
 96 |       "pvs": [
 97 |         {
 98 |           "cp": 190,
 99 |           "line": "h5h6 d8h4 h6g7 f8d8 f4g3 h4g4 c2e4 g4e4 g2e4 g8g7"
100 |         },
101 |         {
102 |           "cp": 186,
103 |           "line": "g2e4 f7f5 e4b7 c8b7 f2f3 b7f3 e1e6 d8h4 c2h2 h4g4"
104 |         },
105 |         {
106 |           "cp": 176,
107 |           "line": "f4g3 f7f5 e1e5 f5f4 g2e4 h7f6 e4b7 c8b7 g3f4 f6g4"
108 |         }
109 |       ],
110 |       "knodes": 162122,
111 |       "depth": 31
112 |     }
113 |   ]
114 | }
115 | 
116 |

Notes

117 |

Evaluations have various depths and node count. If you only want one PV, we recommend selecting the evaluation with the highest depth, and use its first PV.

118 |

119 |
120 |
121 | 128 |

129 | chess puzzles, rated, and tagged. 130 | See them in action on Lichess. 131 |

132 | Download lichess_db_puzzle.csv.zst 133 |

134 |

135 | This file was last updated on . 136 |

137 |

Format

138 |

Puzzles are formatted as standard CSV. The fields are as follows:

139 |
PuzzleId,FEN,Moves,Rating,RatingDeviation,Popularity,NbPlays,Themes,GameUrl,OpeningTags
140 |

Sample

141 |
00sHx,q3k1nr/1pp1nQpp/3p4/1P2p3/4P3/B1PP1b2/B5PP/5K2 b k - 0 17,e8d7 a2e6 d7d8 f7f8,1760,80,83,72,mate mateIn2 middlegame short,https://lichess.org/yyznGmXs/black#34,Italian_Game Italian_Game_Classical_Variation
142 | 00sJ9,r3r1k1/p4ppp/2p2n2/1p6/3P1qb1/2NQR3/PPB2PP1/R1B3K1 w - - 5 18,e3g3 e8e1 g1h2 e1c1 a1c1 f4h6 h2g1 h6c1,2671,105,87,325,advantage attraction fork middlegame sacrifice veryLong,https://lichess.org/gyFeQsOE#35,French_Defense French_Defense_Exchange_Variation
143 | 00sJb,Q1b2r1k/p2np2p/5bp1/q7/5P2/4B3/PPP3PP/2KR1B1R w - - 1 17,d1d7 a5e1 d7d1 e1e3 c1b1 e3b6,2235,76,97,64,advantage fork long,https://lichess.org/kiuvTFoE#33,Sicilian_Defense Sicilian_Defense_Dragon_Variation
144 | 00sO1,1k1r4/pp3pp1/2p1p3/4b3/P3n1P1/8/KPP2PN1/3rBR1R b - - 2 31,b8c7 e1a5 b7b6 f1d1,998,85,94,293,advantage discoveredAttack master middlegame short,https://lichess.org/vsfFkG0s/black#62,
145 |

Notes

146 |

147 | Moves are in UCI format. Use a chess library to convert them to SAN, for display. 148 |

149 |

150 | All player moves of the solution are "only moves". 151 | I.e. playing any other move would considerably worsen the player position. 152 | An exception is made for mates in one: there can be several. Any move that checkmates should win the puzzle. 153 |

154 |

155 | FEN is the position before the opponent makes their move.
156 | The position to present to the player is after applying the first move to that FEN.
157 | The second move is the beginning of the solution. 158 |

159 |

160 | Popularity is a number between 100 (best) and -100 (worst).
161 | It is calculated as 100 * (upvotes - downvotes)/(upvotes + downvotes), although votes are weigthed by various factors such as whether the puzzle was solved successfully or the solver's puzzle rating in comparison to the puzzle's. 162 |

163 |

164 | You can find a list of themes, their names and descriptions, in this file. 165 |

166 |

167 | The OpeningTags field is only set for puzzles starting before move 20. 168 | Here's the list of possible openings. 169 |

170 |

171 | Generating these chess puzzles 172 | took more than 100 years of CPU time.
173 | We went through 600,000,000 analysed games from the Lichess database, 174 | and re-analyzed interesting positions with Stockfish NNUE at 40 meganodes. 175 | The resulting puzzles were then automatically tagged. 176 | To determine the rating, each attempt to solve is considered as a Glicko2 rated game between the player and the puzzle. 177 | Finally, player votes refine the tags and define popularity. 178 |

179 |
180 |
181 | 188 |

Antichess

189 | 190 |

Atomic

191 | 192 |

Chess960

193 | 194 |

Crazyhouse

195 | 196 |

Horde

197 | 198 |

King of the Hill

199 | 200 |

Racing Kings

201 | 202 |

Three-check

203 | 204 |
205 |
206 |
207 | 214 | 215 |
216 | 217 |
218 |

Sample

219 |
[Event "New Zealand Chess Congress 2025"]
220 | [Round "01"]
221 | [White "Metge, J. Nigel"]
222 | [Black "Nagy, Gabor"]
223 | [Result "1/2-1/2"]
224 | [WhiteElo "1885"]
225 | [WhiteFideId "4300181"]
226 | [BlackElo "2469"]
227 | [BlackTitle "GM"]
228 | [BlackFideId "737119"]
229 | [TimeControl "5400+30"]
230 | [Board "1"]
231 | [Variant "Standard"]
232 | [ECO "E47"]
233 | [Opening "Nimzo-Indian Defense: Normal Variation"]
234 | [StudyName "Round 1"]
235 | [ChapterName "Metge, J. Nigel - Nagy, Gabor"]
236 | [UTCDate "2025.01.02"]
237 | [UTCTime "01:29:03"]
238 | [BroadcastName "New Zealand Chess Congress 2025 | Championship"]
239 | [BroadcastURL "https://lichess.org/broadcast/new-zealand-chess-congress-2025--championship/round-1/spI5tcia"]
240 | [GameURL "https://lichess.org/broadcast/new-zealand-chess-congress-2025--championship/round-1/spI5tcia/oHmutTGn"]
241 | 
242 | 1. d4 { [%eval 0.17] [%clk 1:30:50] } 1... Nf6 { [%eval 0.19] [%clk 1:30:39] } 2. c4 { [%eval 0.18] [%clk 1:31:12] } 2... e6 { [%eval 0.11] [%clk 1:30:21] } 3. Nc3 { [%eval 0.17] [%clk 1:31:34] } 3... Bb4 { [%eval 0.09] [%clk 1:28:27] } 4. e3 { [%eval 0.11] [%clk 1:31:38] } 4... O-O { [%eval 0.11] [%clk 1:27:10] } 5. Bd3 { [%eval 0.03] [%clk 1:31:37] } 5... Re8 { [%eval 0.28] [%clk 1:25:53] } 6. Nf3 { [%eval 0.18] [%clk 1:31:03] } 6... d5 { [%eval 0.23] [%clk 1:25:50] } 7. O-O { [%eval 0.2] [%clk 1:27:01] } 7... Nbd7 { [%eval 0.4] [%clk 1:24:53] } 8. Ne5 { [%eval 0.17] [%clk 1:12:36] } 8... dxc4 { [%eval 0.29] [%clk 1:20:40] } 9. Nxc4 { [%eval 0.34] [%clk 1:08:45] } 9... a6 { [%eval 0.23] [%clk 1:15:52] } 10. Qc2 { [%eval 0.16] [%clk 0:52:48] } 10... c5 { [%eval 0.7] [%clk 1:03:58] } 11. Rd1?! { [%eval 0.05] } { Inaccuracy. a3 was best. } { [%clk 0:45:36] } 11... Qc7 { [%eval 0.22] [%clk 0:59:08] } 12. a3 { [%eval 0.0] [%clk 0:27:22] } 12... Bxc3 { [%eval 0.01] [%clk 0:56:30] } 13. bxc3 { [%eval -0.29] [%clk 0:24:44] } 13... b5 { [%eval -0.23] [%clk 0:55:45] } 14. Ne5?! { [%eval -1.05] } { Inaccuracy. Nd2 was best. } { [%clk 0:24:46] } 14... Nxe5 { [%eval -1.08] [%clk 0:55:09] } 15. dxe5 { [%eval -0.99] [%clk 0:25:06] } 15... Qxe5 { [%eval -1.15] [%clk 0:55:20] } 16. f4 { [%eval -1.06] [%clk 0:24:56] } 16... Qc7 { [%eval -1.04] [%clk 0:52:02] } 17. c4 { [%eval -1.06] [%clk 0:24:54] } 17... e5 { [%eval -0.59] [%clk 0:46:02] } 18. cxb5? { [%eval -1.76] } { Mistake. Bb2 was best. } { [%clk 0:21:18] } 18... exf4 { [%eval -1.74] [%clk 0:45:48] } 19. exf4?! { [%eval -2.55] } { Inaccuracy. Rb1 was best. } { [%clk 0:20:49] } 19... Qb6 { [%eval -2.6] [%clk 0:44:49] } 20. Bc4 { [%eval -2.47] [%clk 0:15:07] } 20... axb5 { [%eval -2.32] [%clk 0:44:34] } 21. Qb3?! { [%eval -3.64] } { Inaccuracy. Rb1 was best. } { [%clk 0:15:05] } 21... Qa7 { [%eval -4.06] [%clk 0:44:15] } 22. Qxb5 { [%eval -3.87] [%clk 0:13:39] } 22... Rb8 { [%eval -3.92] [%clk 0:43:20] } 23. Qc6 { [%eval -3.61] [%clk 0:11:51] } 23... Bg4 { [%eval -3.81] [%clk 0:40:29] } 24. Rf1 { [%eval -3.55] [%clk 0:10:56] } 24... Rbc8?? { [%eval -1.35] } { Blunder. Re4 was best. } { [%clk 0:36:46] } 25. Qa6 { [%eval -1.21] [%clk 0:09:58] } 25... Qd7? { [%eval 0.0] } { Mistake. Qe7 was best. } { [%clk 0:36:59] } 26. Bb2 { [%eval 0.0] [%clk 0:09:25] } 26... Ra8 { [%eval 0.0] [%clk 0:35:18] } 27. Qb5 { [%eval 0.03] [%clk 0:09:21] } 27... Qd2 { [%eval 0.13] [%clk 0:33:14] } 28. Qb7 { [%eval -0.18] [%clk 0:01:54] } 28... Nd7 { [%eval 0.28] [%clk 0:29:14] } 29. Rf2 { [%eval 0.34] [%clk 0:01:10] } 29... Qe3 { [%eval 0.36] [%clk 0:28:34] } 30. Qd5?! { [%eval -0.19] } { Inaccuracy. f5 was best. } { [%clk 0:00:58] } 30... Be6 { [%eval -0.24] [%clk 0:27:26] } 31. Qd3 { [%eval -0.22] [%clk 0:00:48] } 31... Bxc4 { [%eval -0.2] [%clk 0:26:04] } 32. Qxc4 { [%eval -0.17] [%clk 0:01:15] } 32... Nb6 { [%eval -0.26] [%clk 0:21:47] } 33. Qc3 { [%eval -0.25] [%clk 0:01:30] } 33... Qxc3 { [%eval -0.19] [%clk 0:22:07] } 34. Bxc3 { [%eval -0.24] [%clk 0:01:59] } 34... Re3 { [%eval -0.01] [%clk 0:22:25] } 35. Rc2 { [%eval -0.03] [%clk 0:02:03] } 35... f6 { [%eval -0.01] [%clk 0:22:07] } 36. Kf2 { [%eval -0.14] [%clk 0:01:42] } 36... Rd3 { [%eval -0.03] [%clk 0:22:06] } 37. Ke2 { [%eval -0.03] [%clk 0:01:18] } 37... c4 { [%eval -0.06] [%clk 0:22:22] } 38. Bb4 { [%eval -0.03] [%clk 0:00:48] } 38... Re8+ { [%eval -0.09] [%clk 0:17:43] } 39. Kf2 { [%eval -0.06] [%clk 0:01:11] } 39... Re4 { [%eval -0.08] [%clk 0:15:48] } 40. g3 { [%eval -0.05] [%clk 0:31:15] } 40... Nd5 { [%eval 0.0] [%clk 0:46:06] } 41. Rac1 { [%eval -0.06] [%clk 0:21:23] } 41... Ne3 { [%eval 0.0] [%clk 0:45:01] } 42. Re2 { [%eval 0.0] [%clk 0:11:27] } 42... f5 { [%eval 0.0] [%clk 0:43:22] } 43. Bc5 { [%eval 0.0] [%clk 0:07:39] } 43... Ng4+ { [%eval 0.0] [%clk 0:39:39] } 44. Ke1 { [%eval 0.0] [%clk 0:07:54] } 44... Rxe2+ { [%eval 0.0] [%clk 0:36:31] } 45. Kxe2 { [%eval 0.0] [%clk 0:08:21] } 45... Nxh2 { [%eval 0.03] [%clk 0:36:51] } 46. Rxc4 { [%eval 0.0] [%clk 0:08:30] } 46... Rxg3 { [%eval 0.0] [%clk 0:37:16] } 47. a4 { [%eval 0.0] [%clk 0:08:01] } 47... h5 { [%eval 0.0] [%clk 0:36:55] } 48. a5 { [%eval 0.0] [%clk 0:08:14] } 48... Rg2+ { [%eval 0.0] [%clk 0:37:19] } 49. Kd3 { [%eval 0.0] [%clk 0:08:01] } 49... Ra2 { [%eval 0.0] [%clk 0:37:41] } 50. Rc2 { [%eval 0.0] [%clk 0:06:57] } 50... Rxa5 { [%eval 0.0] [%clk 0:37:59] } 51. Rxh2 { [%eval 0.0] [%clk 0:07:20] } 51... Rxc5 { [%eval 0.0] [%clk 0:38:21] } 52. Rxh5 { [%eval -0.01] [%clk 0:07:46] } 52... g6 { [%eval -0.05] [%clk 0:38:44] } 53. Rg5 { [%eval -0.07] [%clk 0:08:09] } 53... Kg7 { [%eval -0.01] [%clk 0:38:58] } 54. Kd4 { [%eval 0.0] [%clk 0:08:32] } 54... Rc1 { [%eval 0.0] [%clk 0:39:11] } 55. Ke3 { [%eval 0.0] [%clk 0:08:31] } 55... Kh6 { [%eval 0.0] [%clk 0:37:59] } 56. Kf3 { [%eval 0.0] [%clk 0:06:35] } 56... Rf1+ { [%eval 0.0] [%clk 0:38:19] } 57. Ke3 { [%eval 0.0] [%clk 0:06:48] } 57... Ra1 { [%eval 0.0] [%clk 0:38:12] } 58. Kf3 { [%eval -0.01] [%clk 0:06:50] } 58... Kg7 { [%eval -0.03] [%clk 0:38:35] } 59. Ke3 { [%eval 0.0] [%clk 0:06:59] } 59... Kf6 { [%eval 0.0] [%clk 0:38:43] } 60. Kf3 { [%eval 0.0] [%clk 0:07:21] } 60... Ra3+ { [%eval -0.02] [%clk 0:38:29] } 61. Kf2 { [%eval -0.05] [%clk 0:07:31] } 61... Ke6 { [%eval 0.0] [%clk 0:37:20] } 62. Rxg6+ { [%eval 0.0] [%clk 0:07:05] } 62... Kd5 { [%eval 0.0] [%clk 0:37:44] } 63. Rb6 { [%eval 0.0] [%clk 0:07:00] } 63... Rc3 { [%eval 0.0] [%clk 0:37:42] } 64. Rb5+ { [%eval 0.0] [%clk 0:06:30] } 64... Kd4 { [%eval 0.0] [%clk 0:37:54] } 65. Rxf5 { [%eval 0.0] [%clk 0:06:32] } 65... Ke4 { [%eval 0.0] [%clk 0:38:19] } 66. Re5+ { [%eval 0.0] [%clk 0:05:49] } 66... Kxf4 { [%clk 0:38:41] } 67. Re2 { [%clk 0:06:11] } 67... Rf3+ { [%clk 0:39:07] } 68. Kg2 { [%clk 0:06:36] } 68... Re3 { [%clk 0:39:32] } 69. Rxe3 { [%clk 0:07:02] } 69... Kxe3 { [%clk 0:40:01] } 1/2-1/2
243 |
244 | 245 |
246 |

Notes

247 |

248 | Almost all the games include Stockfish analysis evaluations: 249 | [%eval 2.35] (235 centipawn advantage), 250 | [%eval #-4] (getting mated in 4), 251 | always from White's point of view. 252 |

253 |

254 | Games between chess engines are marked with [WhiteTitle "BOT"] or [BlackTitle "BOT"], respectively. 255 |

256 |
257 | 258 |
259 |

Decompress .zst

260 |

261 | Unix: pzstd -d filename.pgn.zst (faster than unzstd)
262 | Windows: use PeaZip 263 |

264 |

265 | Expect uncompressed files to be about 7.1 times larger. 266 |

267 |

268 | ZStandard archives are partially decompressable, so you can start downloading and then cancel at any point. You will be able to decompress the partial download if you only want a smaller set of game data. 269 |

270 |

271 | You can also decompress the data on-the-fly without having to create large temporary files. This example shows how you can pipe the contents to a Python script for analyzing using zstdcat. 272 |

273 |
$ zstdcat lichess_db.pgn.zst | python script.py
274 |
275 |
276 |
277 |
278 | 285 | 286 |
287 | 288 |
289 |

Sample

290 |
[Event "Rated Bullet tournament https://lichess.org/tournament/yc1WW2Ox"]
291 | [Site "https://lichess.org/PpwPOZMq"]
292 | [Date "2017.04.01"]
293 | [Round "-"]
294 | [White "Abbot"]
295 | [Black "Costello"]
296 | [Result "0-1"]
297 | [UTCDate "2017.04.01"]
298 | [UTCTime "11:32:01"]
299 | [WhiteElo "2100"]
300 | [BlackElo "2000"]
301 | [WhiteRatingDiff "-4"]
302 | [BlackRatingDiff "+1"]
303 | [WhiteTitle "FM"]
304 | [ECO "B30"]
305 | [Opening "Sicilian Defense: Old Sicilian"]
306 | [TimeControl "300+0"]
307 | [Termination "Time forfeit"]
308 | 
309 | 1. e4 { [%eval 0.17] [%clk 0:00:30] } 1... c5 { [%eval 0.19] [%clk 0:00:30] }
310 | 2. Nf3 { [%eval 0.25] [%clk 0:00:29] } 2... Nc6 { [%eval 0.33] [%clk 0:00:30] }
311 | 3. Bc4 { [%eval -0.13] [%clk 0:00:28] } 3... e6 { [%eval -0.04] [%clk 0:00:30] }
312 | 4. c3 { [%eval -0.4] [%clk 0:00:27] } 4... b5? { [%eval 1.18] [%clk 0:00:30] }
313 | 5. Bb3?! { [%eval 0.21] [%clk 0:00:26] } 5... c4 { [%eval 0.32] [%clk 0:00:29] }
314 | 6. Bc2 { [%eval 0.2] [%clk 0:00:25] } 6... a5 { [%eval 0.6] [%clk 0:00:29] }
315 | 7. d4 { [%eval 0.29] [%clk 0:00:23] } 7... cxd3 { [%eval 0.6] [%clk 0:00:27] }
316 | 8. Qxd3 { [%eval 0.12] [%clk 0:00:22] } 8... Nf6 { [%eval 0.52] [%clk 0:00:26] }
317 | 9. e5 { [%eval 0.39] [%clk 0:00:21] } 9... Nd5 { [%eval 0.45] [%clk 0:00:25] }
318 | 10. Bg5?! { [%eval -0.44] [%clk 0:00:18] } 10... Qc7 { [%eval -0.12] [%clk 0:00:23] }
319 | 11. Nbd2?? { [%eval -3.15] [%clk 0:00:14] } 11... h6 { [%eval -2.99] [%clk 0:00:23] }
320 | 12. Bh4 { [%eval -3.0] [%clk 0:00:11] } 12... Ba6? { [%eval -0.12] [%clk 0:00:23] }
321 | 13. b3?? { [%eval -4.14] [%clk 0:00:02] } 13... Nf4? { [%eval -2.73] [%clk 0:00:21] } 0-1
322 |
323 | 324 |
325 |

Notes

326 |

327 | About 6% of the games include Stockfish analysis evaluations: 328 | [%eval 2.35] (235 centipawn advantage), 329 | [%eval #-4] (getting mated in 4), 330 | always from White's point of view. 331 |

332 |

333 | The WhiteElo and BlackElo tags contain Glicko2 ratings. 334 |

335 |

336 | Games contain clock information down to the second as PGN %clk comments since April 2017. If you need centisecond precision, there is a separate export of standard games across all time controls from 2013 to 2021 using %clkc comments. 337 |

338 |

339 | Players using the Bot API 340 | are marked with [WhiteTitle "BOT"] or 341 | [BlackTitle "BOT"], respectively. 342 |

343 |

344 | Variant games have a Variant tag, e.g., [Variant "Antichess"]. 345 |

346 |
347 | 348 |
349 |

Decompress .zst

350 |

351 | Unix: pzstd -d filename.pgn.zst (faster than unzstd)
352 | Windows: use PeaZip 353 |

354 |

355 | Expect uncompressed files to be about 7.1 times larger. 356 |

357 |

358 | ZStandard archives are partially decompressable, so you can start downloading and then cancel at any point. You will be able to decompress the partial download if you only want a smaller set of game data. 359 |

360 |

361 | You can also decompress the data on-the-fly without having to create large temporary files. This example shows how you can pipe the contents to a Python script for analyzing using zstdcat. 362 |

363 |
$ zstdcat lichess_db.pgn.zst | python script.py
364 |
365 | 366 |
367 |

Open PGN files

368 |

369 | Traditional PGN databases, like SCID or ChessBase, fail to open large PGN files. 370 | Until they fix it, you can split the PGN files, 371 | or use programmatic APIs such as python-chess 372 | or Scoutfish. 373 |

374 |
375 | 376 |
377 |

Known issues

378 | 428 |
429 |
430 | 431 |
432 | 433 | 542 |

543 | Did you use this database? Please share your results! contact@lichess.org 544 |

545 |
546 |
547 | 550 |
551 | 552 | 553 | -------------------------------------------------------------------------------- /web/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | const prettyBytes = require('pretty-bytes'); 3 | const moment = require('moment'); 4 | 5 | const sourceDir = process.argv[2]; 6 | const indexFile = 'index.html'; 7 | const indexTpl = indexFile + '.tpl'; 8 | const tableTpl = 'table.html.tpl'; 9 | const broadcastTableTpl = 'broadcast-table.html.tpl'; 10 | const styleFile = 'style.css'; 11 | const listFile = 'list.txt'; 12 | 13 | const clockSince = moment('2017-04'); 14 | const today = new Date(); 15 | 16 | function numberFormat(n) { 17 | return new Intl.NumberFormat().format(n); 18 | } 19 | 20 | function fileInfo(counts, dir, n) { 21 | const path = sourceDir + '/' + dir + '/' + n; 22 | return fs.stat(path).then(s => { 23 | const dateStr = n.replace(/.+(\d{4}-\d{2})\.pgn\.zst/, '$1'); 24 | const m = moment(dateStr); 25 | const hasClock = m.unix() >= clockSince.unix(); 26 | return { 27 | name: n, 28 | path: path, 29 | size: s.size, 30 | date: m, 31 | clock: hasClock, 32 | games: parseInt(counts[n]) || 0, 33 | }; 34 | }); 35 | } 36 | 37 | function getGameCounts(variant) { 38 | return fs.readFile(sourceDir + '/' + variant + '/counts.txt', { encoding: 'utf8' }).then(c => { 39 | var gameCounts = {}; 40 | c.split('\n') 41 | .map(l => l.trim()) 42 | .forEach(line => { 43 | if (line !== '') gameCounts[line.split(' ')[0]] = line.split(' ')[1]; 44 | }); 45 | return gameCounts; 46 | }); 47 | } 48 | 49 | function getFiles(variant) { 50 | return function(gameCounts) { 51 | return fs 52 | .readdir(sourceDir + '/' + variant) 53 | .then(items => { 54 | return Promise.all(items.filter(n => n.endsWith('.pgn.zst')).map(n => fileInfo(gameCounts, variant, n))); 55 | }) 56 | .then(items => items.sort((a, b) => b.date.unix() - a.date.unix())); 57 | }; 58 | } 59 | 60 | function renderTable(files, dir) { 61 | return files 62 | .map(f => { 63 | return ` 64 | ${f.date.format('YYYY - MMMM')} 65 | ${prettyBytes(f.size)} 66 | ${f.games ? numberFormat(f.games) : '?'} 67 | .pgn.zst / .torrent 69 | `; 70 | }) 71 | .join('\n'); 72 | } 73 | 74 | function renderBroadcastTable(files, dir) { 75 | return files 76 | .map(f => { 77 | return ` 78 | ${f.date.format('YYYY - MMMM')} 79 | ${prettyBytes(f.size)} 80 | ${f.games ? numberFormat(f.games) : '?'} 81 | 👁 82 | .pgn.zst 83 | `; 84 | }) 85 | .join('\n'); 86 | } 87 | 88 | function renderTotal(files) { 89 | return ` 90 | Total: ${files.length} files 91 | ${prettyBytes(files.map(f => f.size).reduce((a, b) => a + b, 0))} 92 | ${numberFormat(files.map(f => f.games).reduce((a, b) => a + b, 0))} 93 | 94 | 95 | `; 96 | } 97 | 98 | function renderList(files, dir) { 99 | return files 100 | .map(f => `https://database.lichess.org/${dir}/${f.name}`) 101 | .join('\n'); 102 | } 103 | 104 | function processVariantAndReturnTable(variant, template) { 105 | return getGameCounts(variant) 106 | .then(getFiles(variant)) 107 | .then(files => { 108 | return fs.writeFile(sourceDir + '/' + variant + '/' + listFile, renderList(files, variant)).then(_ => { 109 | return template 110 | .replace(//, numberFormat(files.map(f => f.games).reduce((a, b) => a + b, 0))) 111 | .replace(//, renderTable(files, variant)) 112 | .replace(//, renderTotal(files)) 113 | .replace(//g, variant); 114 | }); 115 | }); 116 | } 117 | 118 | function replaceVariant(variant, tableTemplate) { 119 | return function(fullTemplate) { 120 | return processVariantAndReturnTable(variant, tableTemplate).then(tbl => { 121 | return fullTemplate.replace('', tbl); 122 | }); 123 | }; 124 | } 125 | 126 | function getBroadcastCounts() { 127 | return fs.readFile(sourceDir + '/broadcast/counts.txt', { encoding: 'utf8' }).then(c => { 128 | var counts = {}; 129 | c.split('\n') 130 | .map(l => l.trim()) 131 | .forEach(line => { 132 | if (line !== '') counts[line.split(' ')[0]] = line.split(' ')[1]; 133 | }); 134 | return counts; 135 | }); 136 | } 137 | 138 | function getBroadcastFiles() { 139 | return function(counts) { 140 | return fs 141 | .readdir(sourceDir + '/broadcast') 142 | .then(items => Promise.all(items.filter(n => n.endsWith('.pgn.zst')).map(n => fileInfo(counts, 'broadcast', n)))) 143 | .then(items => items.sort((a, b) => b.date.unix() - a.date.unix())); 144 | }; 145 | } 146 | 147 | function processBroadcasts(template) { 148 | return getBroadcastCounts() 149 | .then(getBroadcastFiles()) 150 | .then(files => { 151 | return fs.writeFile(sourceDir + '/broadcast/' + listFile, renderList(files, 'broadcast')).then(_ => { 152 | return template 153 | .replace(//, numberFormat(files.map(f => f.games).reduce((a, b) => a + b, 0))) 154 | .replace(//, renderBroadcastTable(files, 'broadcast')) 155 | .replace(//, renderTotal(files)); 156 | }); 157 | }); 158 | } 159 | 160 | function replaceBroadcasts(tableTemplate) { 161 | return function(fullTemplate) { 162 | return processBroadcasts(tableTemplate).then(tbl => { 163 | return fullTemplate.replace('', tbl); 164 | }); 165 | }; 166 | } 167 | 168 | function replaceNbPuzzles(template) { 169 | return fs 170 | .readFile(sourceDir + '/puzzle-count.txt', { encoding: 'utf8' }) 171 | .then(c => template.replace('', numberFormat(c))); 172 | } 173 | 174 | function replaceNbEvals(template) { 175 | return fs 176 | .readFile(sourceDir + '/eval-count.txt', { encoding: 'utf8' }) 177 | .then(c => template.replace('', numberFormat(parseInt(c)))); 178 | } 179 | 180 | function replaceDateUpdated(template) { 181 | return template.replace(//g, today.toISOString().split('T')[0]); 182 | } 183 | 184 | process.on('unhandledRejection', r => console.log(r)); 185 | 186 | Promise.all([ 187 | fs.readFile(indexTpl, { encoding: 'utf8' }), 188 | fs.readFile(tableTpl, { encoding: 'utf8' }), 189 | fs.readFile(broadcastTableTpl, { encoding: 'utf8' }), 190 | fs.readFile(styleFile, { encoding: 'utf8' }), 191 | ]).then(([index, table, broadcastTable, style]) => { 192 | const rv = variant => replaceVariant(variant, table); 193 | return rv('standard')(index) 194 | .then(rv('antichess')) 195 | .then(rv('atomic')) 196 | .then(rv('chess960')) 197 | .then(rv('crazyhouse')) 198 | .then(rv('horde')) 199 | .then(rv('kingOfTheHill')) 200 | .then(rv('racingKings')) 201 | .then(rv('threeCheck')) 202 | .then(replaceBroadcasts(broadcastTable)) 203 | .then(replaceNbPuzzles) 204 | .then(replaceNbEvals) 205 | .then(replaceDateUpdated) 206 | .then(rendered => { 207 | return fs.writeFile(sourceDir + '/' + indexFile, rendered.replace(//, style)); 208 | }); 209 | }); 210 | -------------------------------------------------------------------------------- /web/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lichess-db", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "fs-extra": { 8 | "version": "3.0.1", 9 | "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-3.0.1.tgz", 10 | "integrity": "sha1-N5TzeMWLNC6n27sjCVEJxLO2IpE=", 11 | "requires": { 12 | "graceful-fs": "^4.1.2", 13 | "jsonfile": "^3.0.0", 14 | "universalify": "^0.1.0" 15 | } 16 | }, 17 | "graceful-fs": { 18 | "version": "4.1.11", 19 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", 20 | "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" 21 | }, 22 | "jsonfile": { 23 | "version": "3.0.1", 24 | "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz", 25 | "integrity": "sha1-pezG9l9T9mLEQVx2daAzHQmS7GY=", 26 | "requires": { 27 | "graceful-fs": "^4.1.6" 28 | } 29 | }, 30 | "moment": { 31 | "version": "2.29.4", 32 | "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", 33 | "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==" 34 | }, 35 | "pretty-bytes": { 36 | "version": "4.0.2", 37 | "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-4.0.2.tgz", 38 | "integrity": "sha1-sr+C5zUNZcbDOqlaqlpPYyf2HNk=" 39 | }, 40 | "universalify": { 41 | "version": "0.1.1", 42 | "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz", 43 | "integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc=" 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lichess-db", 3 | "version": "1.0.0", 4 | "description": "lichess.org game database", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "node index.js" 8 | }, 9 | "author": "Thibault Duplessis", 10 | "license": "ISC", 11 | "dependencies": { 12 | "fs-extra": "^3.0.1", 13 | "moment": "^2.29.4", 14 | "pretty-bytes": "^4.0.2" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /web/style.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ v2.0 | 20110126 License: none (public domain) 2 | */ 3 | html, 4 | body, 5 | div, 6 | span, 7 | applet, 8 | object, 9 | iframe, 10 | h1, 11 | h2, 12 | h3, 13 | h4, 14 | h5, 15 | h6, 16 | p, 17 | blockquote, 18 | pre, 19 | a, 20 | abbr, 21 | acronym, 22 | address, 23 | big, 24 | cite, 25 | code, 26 | del, 27 | dfn, 28 | em, 29 | img, 30 | ins, 31 | kbd, 32 | q, 33 | s, 34 | samp, 35 | small, 36 | strike, 37 | strong, 38 | sub, 39 | sup, 40 | tt, 41 | var, 42 | b, 43 | u, 44 | i, 45 | center, 46 | dl, 47 | dt, 48 | dd, 49 | ol, 50 | ul, 51 | li, 52 | fieldset, 53 | form, 54 | label, 55 | legend, 56 | table, 57 | caption, 58 | tbody, 59 | tfoot, 60 | thead, 61 | tr, 62 | th, 63 | td, 64 | article, 65 | aside, 66 | canvas, 67 | details, 68 | embed, 69 | figure, 70 | figcaption, 71 | footer, 72 | header, 73 | hgroup, 74 | menu, 75 | nav, 76 | output, 77 | ruby, 78 | section, 79 | summary, 80 | time, 81 | mark, 82 | audio, 83 | video { 84 | padding: 0; 85 | margin: 0; 86 | font: inherit; 87 | font-size: 100%; 88 | vertical-align: baseline; 89 | border: 0; 90 | } 91 | 92 | /* HTML5 display-role reset for older browsers */ 93 | article, 94 | aside, 95 | details, 96 | figcaption, 97 | figure, 98 | footer, 99 | header, 100 | hgroup, 101 | menu, 102 | nav, 103 | section { 104 | display: block; 105 | } 106 | 107 | body { 108 | line-height: 1; 109 | } 110 | 111 | ol, 112 | ul { 113 | list-style: none; 114 | margin-top: 1em; 115 | } 116 | 117 | blockquote, 118 | q { 119 | quotes: none; 120 | } 121 | 122 | blockquote:before, 123 | blockquote:after, 124 | q:before, 125 | q:after { 126 | content: ''; 127 | content: none; 128 | } 129 | 130 | table { 131 | border-spacing: 0; 132 | border-collapse: collapse; 133 | } 134 | 135 | /* LAYOUT STYLES */ 136 | body { 137 | font-family: 'Noto Sans', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, Sans-Serif; 138 | font-size: 1.1em; 139 | line-height: 1.5; 140 | background: #262e36; 141 | color: rgb(173, 177, 181); 142 | --c-primary: rgb(147, 204, 127); 143 | } 144 | 145 | a { 146 | color: #f0f0f0; 147 | } 148 | 149 | a:hover { 150 | color: #fff; 151 | } 152 | 153 | .primary { 154 | color: var(--c-primary); 155 | font-weight: bold; 156 | } 157 | 158 | .big-header { 159 | background: rgb(25, 31, 36); 160 | padding-bottom: 7em; 161 | } 162 | 163 | .content, 164 | .license, 165 | header { 166 | width: 750px; 167 | margin: 0 auto; 168 | } 169 | 170 | header { 171 | padding: 70px 0 50px 0; 172 | display: flex; 173 | align-items: center; 174 | } 175 | 176 | header img { 177 | margin: 0 50px 0 -210px; 178 | width: 160px; 179 | height: 160px; 180 | opacity: 0.3; 181 | } 182 | 183 | @media (max-width: 1000px) { 184 | header img { 185 | display: none; 186 | } 187 | } 188 | 189 | header h1 { 190 | font-family: Roboto; 191 | line-height: 1.2; 192 | color: #e0e0e0; 193 | display: flex; 194 | flex-flow: column; 195 | } 196 | 197 | header h1 a { 198 | color: #e0e0e0; 199 | font-size: 66px; 200 | letter-spacing: 10px; 201 | margin-bottom: 12px; 202 | } 203 | 204 | header h1 strong { 205 | font-size: 28px; 206 | font-weight: lighter; 207 | letter-spacing: 10px; 208 | } 209 | 210 | .license { 211 | font-size: 0.9em; 212 | } 213 | 214 | #downloads { 215 | margin-bottom: 40px; 216 | } 217 | 218 | #download-zip span { 219 | background: transparent url(../images/zip-icon.png) 12px 50% no-repeat; 220 | } 221 | 222 | #download-tar-gz span { 223 | background: transparent url(../images/tar-gz-icon.png) 12px 50% no-repeat; 224 | } 225 | 226 | #view-on-github span { 227 | background: transparent url(../images/octocat-icon.png) 12px 50% no-repeat; 228 | } 229 | 230 | #view-on-github { 231 | margin-right: 0; 232 | } 233 | 234 | code, 235 | pre { 236 | margin-bottom: 30px; 237 | font-family: 'Bitstream Vera Sans Mono', 'Lucida Console', Terminal, Monaco, monospace; 238 | background: #222; 239 | border-radius: 4px; 240 | font-size: 0.9em; 241 | line-height: 1.3; 242 | } 243 | 244 | code { 245 | padding: 2px 5px; 246 | } 247 | 248 | pre { 249 | padding: 20px; 250 | overflow: auto; 251 | } 252 | 253 | pre code { 254 | padding: 0; 255 | border: none; 256 | } 257 | 258 | ul, 259 | ol, 260 | dl { 261 | margin-bottom: 20px; 262 | } 263 | 264 | span.sep { 265 | color: #e0e0e0; 266 | } 267 | 268 | /* COMMON STYLES */ 269 | hr { 270 | height: 1px; 271 | padding-bottom: 1em; 272 | margin-top: 1em; 273 | line-height: 1px; 274 | background: rgb(25, 31, 36); 275 | border: none; 276 | } 277 | 278 | strong { 279 | font-weight: bold; 280 | } 281 | 282 | em { 283 | font-style: italic; 284 | } 285 | 286 | table { 287 | width: 100%; 288 | margin-top: 40px; 289 | } 290 | 291 | th { 292 | font-weight: 500; 293 | } 294 | 295 | th:first-child { 296 | padding-left: 0; 297 | } 298 | 299 | td { 300 | font-weight: 300; 301 | text-align: center; 302 | border-top: 1px solid rgb(25, 31, 36); 303 | line-height: 2em; 304 | } 305 | 306 | td:first-child { 307 | padding-left: 0; 308 | } 309 | 310 | td:last-child { 311 | padding-right: 0; 312 | } 313 | 314 | form { 315 | padding: 20px; 316 | background: #f2f2f2; 317 | } 318 | 319 | /* GENERAL ELEMENT TYPE STYLES */ 320 | h1 { 321 | font-size: 32px; 322 | } 323 | 324 | h2 { 325 | margin-bottom: 8px; 326 | font-size: 22px; 327 | font-weight: bold; 328 | color: #93cc7f; 329 | } 330 | 331 | h3 { 332 | margin-top: 2em; 333 | margin-bottom: 0.7em; 334 | font-size: 30px; 335 | color: var(--c-primary); 336 | letter-spacing: 7px; 337 | border-bottom: 1px solid var(--c-primary); 338 | } 339 | 340 | h4 { 341 | font-size: 16px; 342 | font-weight: bold; 343 | color: #93cc7f; 344 | } 345 | 346 | h5 { 347 | font-size: 1em; 348 | color: #303030; 349 | } 350 | 351 | h6 { 352 | font-size: 0.8em; 353 | color: #303030; 354 | } 355 | 356 | p { 357 | margin-bottom: 20px; 358 | font-weight: 300; 359 | } 360 | 361 | a { 362 | text-decoration: none; 363 | } 364 | 365 | p a { 366 | font-weight: 400; 367 | } 368 | 369 | blockquote { 370 | padding: 0 0 0 30px; 371 | margin-bottom: 20px; 372 | font-size: 1.6em; 373 | border-left: 10px solid #e9e9e9; 374 | } 375 | 376 | ul li { 377 | list-style: outside disc; 378 | margin: 10px 0 0 20px; 379 | } 380 | 381 | ol li { 382 | list-style-position: inside; 383 | list-style: decimal; 384 | padding-left: 3px; 385 | } 386 | 387 | dl dt { 388 | color: #303030; 389 | } 390 | 391 | footer { 392 | text-align: center; 393 | padding-top: 6em; 394 | padding-bottom: 6em; 395 | margin-top: 3em; 396 | font-size: 13px; 397 | background: rgb(25, 31, 36); 398 | } 399 | 400 | footer a { 401 | color: var(--c-primary); 402 | } 403 | 404 | table { 405 | margin-bottom: 20px; 406 | } 407 | 408 | table, 409 | table td { 410 | text-align: left; 411 | } 412 | 413 | table th { 414 | font-family: Roboto; 415 | } 416 | 417 | table th, 418 | table td { 419 | padding: 3px 10px; 420 | } 421 | 422 | table .center { 423 | text-align: center; 424 | } 425 | 426 | table .right { 427 | text-align: right; 428 | } 429 | 430 | table .total td { 431 | font-weight: bold; 432 | } 433 | 434 | table td a { 435 | color: var(--c-primary); 436 | font-weight: bold; 437 | } 438 | 439 | table td a:hover { 440 | text-decoration: underline; 441 | } 442 | 443 | nav { 444 | padding: 1em 0; 445 | margin-top: -3.2em; 446 | } 447 | 448 | nav a { 449 | cursor: pointer; 450 | padding: 0.6em 0 0.6em; 451 | margin-right: 1.2em; 452 | text-transform: uppercase; 453 | border-bottom: 0.2em solid transparent; 454 | letter-spacing: 1px; 455 | color: #adb1b5; 456 | } 457 | 458 | nav a:hover { 459 | color: var(--c-primary); 460 | } 461 | 462 | nav a.on { 463 | color: var(--c-primary); 464 | border-color: var(--c-primary); 465 | } 466 | 467 | #evals, 468 | #puzzles, 469 | #broadcasts, 470 | #variant_games, 471 | #evals:target~#standard_games, 472 | #puzzles:target~#standard_games, 473 | #broadcasts:target~#standard_games, 474 | #variant_games:target~#standard_games { 475 | display: none; 476 | } 477 | 478 | #evals:target, 479 | #puzzles:target, 480 | #broadcasts:target, 481 | #variant_games:target, 482 | #standard_games, 483 | #standard_games:target { 484 | display: block; 485 | } 486 | 487 | #standard_games nav, 488 | #broadcasts nav, 489 | #evals nav, 490 | #puzzles nav { 491 | margin-bottom: 50px; 492 | } 493 | -------------------------------------------------------------------------------- /web/table.html.tpl: -------------------------------------------------------------------------------- 1 |

2 | rated games, played on lichess.org, in PGN format. 3 | Each file contains the games for one month only; they are not cumulative. 4 |

5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
MonthSizeGamesDownload
20 | 21 |

22 | Here's a plain text download list, 23 | and the SHA256 checksums. 24 |

25 | -------------------------------------------------------------------------------- /web/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | fs-extra@^3.0.1: 6 | version "3.0.1" 7 | resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-3.0.1.tgz#3794f378c58b342ea7dbbb23095109c4b3b62291" 8 | integrity sha1-N5TzeMWLNC6n27sjCVEJxLO2IpE= 9 | dependencies: 10 | graceful-fs "^4.1.2" 11 | jsonfile "^3.0.0" 12 | universalify "^0.1.0" 13 | 14 | graceful-fs@^4.1.2, graceful-fs@^4.1.6: 15 | version "4.2.4" 16 | resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" 17 | integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== 18 | 19 | jsonfile@^3.0.0: 20 | version "3.0.1" 21 | resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-3.0.1.tgz#a5ecc6f65f53f662c4415c7675a0331d0992ec66" 22 | integrity sha1-pezG9l9T9mLEQVx2daAzHQmS7GY= 23 | optionalDependencies: 24 | graceful-fs "^4.1.6" 25 | 26 | moment@^2.29.4: 27 | version "2.29.4" 28 | resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" 29 | integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== 30 | 31 | pretty-bytes@^4.0.2: 32 | version "4.0.2" 33 | resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-4.0.2.tgz#b2bf82e7350d65c6c33aa95aaa5a4f6327f61cd9" 34 | integrity sha1-sr+C5zUNZcbDOqlaqlpPYyf2HNk= 35 | 36 | universalify@^0.1.0: 37 | version "0.1.2" 38 | resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" 39 | integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== 40 | --------------------------------------------------------------------------------