├── .gitignore ├── LICENSE ├── README.md ├── chess-nginx.conf ├── chess.service ├── chess_website.service ├── chessbot ├── __init__.py ├── bot.py ├── command.py ├── commands │ ├── __init__.py │ ├── announcement.py │ ├── badge.py │ ├── blacklist.py │ ├── board.py │ ├── coinflip.py │ ├── debug.py │ ├── endgame.py │ ├── game.py │ ├── help.py │ ├── leaderboard.py │ ├── links.py │ ├── manage.py │ ├── move.py │ ├── newgame.py │ ├── pocket.py │ ├── prefix.py │ ├── profile.py │ ├── restart.py │ ├── setactivity.py │ ├── status.py │ ├── suggestion.py │ ├── takeback.py │ └── tournament.py ├── config.py ├── db.py ├── glicko2.py ├── parameter.py ├── util.py └── website │ ├── __init__.py │ ├── modules │ ├── __init__.py │ ├── api.py │ └── home.py │ ├── static │ ├── css │ │ ├── big_stats.css │ │ ├── commands.css │ │ ├── home.css │ │ ├── leaderboard.css │ │ ├── main.css │ │ └── user.css │ └── js │ │ └── home.js │ └── templates │ ├── big_stats.html │ ├── commands.html │ ├── home.html │ ├── layout.html │ ├── leaderboard.html │ └── user.html ├── requirements.txt ├── run_chessbot.py └── run_website.py /.gitignore: -------------------------------------------------------------------------------- 1 | tok.py 2 | __pycache__ 3 | commands/__pycache__ 4 | .pyc 5 | 6 | venv 7 | 8 | .vscode 9 | 10 | .swp 11 | -------------------------------------------------------------------------------- /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 | ChessBot, a programing for playing a game of Chess in a Discord server 633 | Copyright (C) 2020 qwertyquerty 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 | . -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ChessBot 2 | The official repository for my Discord ChessBot 3 | 4 | [![https://top.gg/api/widget/366770566331629579.svg](https://top.gg/api/widget/366770566331629579.svg)](https://top.gg/bot/chess) 5 | 6 | ## Install Instructions 7 | 8 | 1. Clone this repo with `git clone https://github.com/qwertyquerty/ChessBot` 9 | 2. Make a virtual environment, and activate it 10 | 3. Install all requirements with `pip3 install -r requirements.txt` 11 | 4. Run it with `python3 run_chessbot.py` -------------------------------------------------------------------------------- /chess-nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name chess.qtqt.cf; 4 | 5 | location / { 6 | proxy_pass http://localhost:7003; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /chess.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Chess Bot 3 | After=multi-user.target 4 | 5 | [Service] 6 | Type=simple 7 | Restart=always 8 | WorkingDirectory=/chess-bot 9 | ExecStart=/usr/bin/python3.8 /chess-bot/run_chessbot.py 10 | 11 | [Install] 12 | WantedBy=multi-user.target 13 | -------------------------------------------------------------------------------- /chess_website.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Chess Bot Website 3 | After=multi-user.target 4 | 5 | [Service] 6 | Type=simple 7 | Restart=always 8 | WorkingDirectory=/chess-bot 9 | ExecStart=/usr/bin/python3.8 /chess-bot/run_website.py 10 | 11 | [Install] 12 | WantedBy=multi-user.target 13 | -------------------------------------------------------------------------------- /chessbot/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qwertyquerty/ChessBot/1f126fc9b2ddaec49603a1765bdc689b98e4a16f/chessbot/__init__.py -------------------------------------------------------------------------------- /chessbot/bot.py: -------------------------------------------------------------------------------- 1 | from chessbot.command import Command 2 | from chessbot.commands import * 3 | from chessbot.config import * 4 | from chessbot import db 5 | from chessbot.util import * 6 | 7 | import elasticapm 8 | 9 | import discord 10 | import traceback 11 | 12 | class ChessBot(discord.AutoShardedClient): 13 | def __init__(self, pid=None, **kwargs): 14 | super().__init__(**kwargs) 15 | 16 | self.pid = pid 17 | self.prefix_cache = {} 18 | self.command_list = Command.__subclasses__() 19 | 20 | self.log_channel = None 21 | self.error_channel = None 22 | 23 | if APM_SERVICE: 24 | self.apm = elasticapm.Client({'SERVICE_NAME': APM_SERVICE}) 25 | else: 26 | self.apm = None 27 | 28 | print("Process {} created for shards {}".format(self.pid, self.shard_ids)) 29 | 30 | async def on_ready(self): 31 | 32 | print("Process {} ready on shards {}".format(self.pid, self.shard_ids)) 33 | 34 | self.log_channel = await self.fetch_channel(LOGCHANNEL) 35 | self.error_channel = await self.fetch_channel(ERRORCHANNEL) 36 | 37 | await update_activity(self) 38 | 39 | await send_dbl_stats(self) 40 | 41 | async def on_guild_join(self, guild): 42 | db.Guild.new(guild.id,guild.name) 43 | await send_dbl_stats(self) 44 | 45 | async def on_guild_remove(self, guild): 46 | await send_dbl_stats(self) 47 | 48 | async def on_guild_update(self, before, after): 49 | guild = db.Guild.from_guild_id(after.id) 50 | if after.name != guild.name: guild.set("name", after.name) 51 | 52 | async def on_message(self, message): 53 | ctx = Ctx() 54 | ctx.bot = self 55 | ctx.msg = message 56 | ctx.message = ctx.msg 57 | ctx.mem = ctx.msg.author 58 | ctx.content = ctx.msg.content 59 | ctx.ch = ctx.msg.channel 60 | ctx.channel = ctx.ch 61 | 62 | try: 63 | ctx.guild = ctx.msg.guild 64 | ctx.mentions = ctx.msg.mentions 65 | 66 | ctx.dbguild = None 67 | 68 | if ctx.guild.id in self.prefix_cache: 69 | ctx.prefix = self.prefix_cache[ctx.guild.id] 70 | else: 71 | ctx.dbguild = db.Guild.from_guild(ctx.guild) 72 | ctx.prefix = ctx.dbguild.prefix 73 | self.prefix_cache[ctx.guild.id] = ctx.dbguild.prefix 74 | 75 | if not ctx.mem.bot and ctx.content.startswith(ctx.prefix): 76 | 77 | ctx.raw_args = ' '.join(ctx.msg.content[len(ctx.prefix):].split()).split() 78 | ctx.args = [] 79 | 80 | if len(ctx.raw_args) == 0: return 81 | 82 | ctx.command = ctx.raw_args.pop(0).lower() 83 | 84 | for cmd in self.command_list: 85 | if ctx.command == cmd.name or ctx.command in cmd.aliases: 86 | 87 | ### Make the bot type while it works out the command 88 | await ctx.ch.trigger_typing() 89 | 90 | ### Update user name and guild name if needed when a viable command is found 91 | ctx.user = db.User.from_mem(ctx.mem) 92 | if ctx.user.name != str(ctx.mem): ctx.user.set("name", str(ctx.mem)) 93 | 94 | if ctx.dbguild == None: 95 | ctx.dbguild = db.Guild.from_guild(ctx.guild) 96 | 97 | if ctx.guild.name != ctx.dbguild.name: ctx.dbguild.set("name", ctx.guild.name) 98 | 99 | if self.apm: 100 | self.apm.begin_transaction("command") 101 | elasticapm.set_transaction_name("command.{}".format(cmd.name), override=False) 102 | 103 | ### Actually call the command 104 | await cmd.call(ctx) 105 | 106 | if ctx.guild != None: 107 | if self.log_channel: 108 | await log_command(ctx) 109 | if self.apm: 110 | self.apm.end_transaction("command", "success") 111 | 112 | break 113 | 114 | except Exception as E: 115 | if type(E) == discord.errors.Forbidden: 116 | await ctx.mem.send("I don't have permissions to talk in that channel! I need: ```Read Messages, Send Messages, Embed Links, Upload files, Add reactions``` If you do not have permission to change these, talk to the server owner.") 117 | elif type(E) == UnboundLocalError: 118 | await ctx.mem.send("I don't do DMs nerd") 119 | else: 120 | if self.apm: 121 | self.apm.capture_exception() 122 | 123 | if self.error_channel: 124 | await log_error(ctx.bot, ctx.msg, traceback.format_exc()) 125 | -------------------------------------------------------------------------------- /chessbot/command.py: -------------------------------------------------------------------------------- 1 | from chessbot import config 2 | from chessbot.config import * 3 | from chessbot import db 4 | from chessbot.parameter import * 5 | from chessbot.util import * 6 | 7 | import discord 8 | import chess 9 | import random 10 | import psutil 11 | import datetime 12 | import time 13 | import traceback 14 | import os 15 | import math 16 | import re 17 | from bson.objectid import ObjectId 18 | 19 | class Command(): 20 | name = "command" 21 | flags = FLAG_NONE 22 | level = LEVEL_EVERYONE 23 | aliases = [] 24 | enabled = True 25 | help_string = None 26 | help_index = 0 27 | parameters = [] 28 | 29 | @classmethod 30 | async def call(cls, ctx): 31 | if ctx.user.level < cls.level: 32 | await ctx.ch.send("You do not have permission to run this command!") 33 | return 34 | 35 | if cls.flags & FLAG_MUST_BE_IN_GAME and not ctx.game: 36 | await ctx.ch.send("You are not in a game! Make one with `{prefix}newgame `".format(prefix=ctx.prefix)) 37 | return 38 | 39 | if cls.flags & FLAG_MUST_BE_SERVER_OWNER and ctx.mem != ctx.guild.owner: 40 | await ctx.ch.send("You must be the server owner to do this!") 41 | return 42 | 43 | if cls.flags & FLAG_MUST_HAVE_PERM_MANAGE_SERVER and not ctx.mem.guild_permissions.manage_guild: 44 | await ctx.ch.send("You must have the permission `manage server` to do this!") 45 | return 46 | 47 | if cls.flags & FLAG_MUST_NOT_BE_BLACKLISTED and ctx.user.flags & USER_FLAG_BLACKLISTED: 48 | await ctx.ch.send("You cannot run this command while blacklisted!") 49 | return 50 | 51 | arg_num = 0 52 | 53 | for param in cls.parameters: 54 | ctx.args.append(None) 55 | 56 | if len(ctx.raw_args) >= (arg_num + 1): 57 | arg = ctx.raw_args[arg_num] 58 | parsed_arg = await param.parse(ctx, arg) 59 | 60 | if parsed_arg == None: 61 | await ctx.ch.send("Invalid input for: `{}` of type `{}`! {} **Usage:** `{}{}`".format(param.name, param.type_name, param.usage_string(), ctx.prefix, cls.usage_string())) 62 | return 63 | 64 | ctx.args[arg_num] = parsed_arg 65 | 66 | elif not param.required: 67 | ctx.args[arg_num] = None 68 | 69 | else: 70 | await ctx.ch.send("You must specify: `{}` of type `{}`! **Usage:** `{}{}`".format(param.name, param.type_name, ctx.prefix, cls.usage_string())) 71 | return 72 | 73 | arg_num += 1 74 | 75 | 76 | await cls.run(ctx) 77 | 78 | @classmethod 79 | def usage_string(cls): 80 | usage_str = cls.name 81 | for param in cls.parameters: 82 | usage_str += " " 83 | if param.required: 84 | usage_str += "<{}>".format(param.name) 85 | else: 86 | usage_str += "[{}]".format(param.name) 87 | 88 | return usage_str 89 | 90 | @classmethod 91 | async def run(cls,ctx): 92 | pass 93 | 94 | 95 | -------------------------------------------------------------------------------- /chessbot/commands/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import glob 3 | modules = glob.glob(os.path.join(os.path.dirname(__file__), "*.py")) 4 | __all__ = [os.path.basename(f)[:-3] for f in modules if os.path.isfile(f) and not f.endswith('__init__.py')] -------------------------------------------------------------------------------- /chessbot/commands/announcement.py: -------------------------------------------------------------------------------- 1 | from chessbot.command import * 2 | 3 | class CommandUnsubscribe(Command): 4 | name = "unsubscribe" 5 | help_string = "Unsubscribe your guild from notifications" 6 | help_index = 500 7 | flags = FLAG_MUST_HAVE_PERM_MANAGE_SERVER 8 | 9 | @classmethod 10 | async def run(cls,ctx): 11 | ctx.dbguild.set("subscribed", False) 12 | 13 | await ctx.ch.send("You have unsubscribed your guild from ChessBot notifications! You can resubscribe with {}subcribe".format(ctx.prefix)) 14 | 15 | class CommandSubscribe(Command): 16 | name = "subscribe" 17 | help_string = "Subscribe your guild to notifications" 18 | help_index = 480 19 | flags = FLAG_MUST_HAVE_PERM_MANAGE_SERVER 20 | 21 | @classmethod 22 | async def run(cls,ctx): 23 | ctx.dbguild.set("subscribed", True) 24 | 25 | await ctx.ch.send("You have subscribed your guild from ChessBot notifications!") -------------------------------------------------------------------------------- /chessbot/commands/badge.py: -------------------------------------------------------------------------------- 1 | from chessbot.command import * 2 | 3 | class CommandBadge(Command): 4 | name = "badge" 5 | help_string = "View the name of a badge" 6 | help_index = 290 7 | parameters = [ParamString("emoji")] 8 | 9 | @classmethod 10 | async def run(cls,ctx): 11 | try: 12 | await ctx.ch.send([key for key, value in config.BADGES.items() if value == ctx.args[0]][0].replace("-"," ").title()) 13 | except: 14 | await ctx.ch.send('Badge not found!') 15 | -------------------------------------------------------------------------------- /chessbot/commands/blacklist.py: -------------------------------------------------------------------------------- 1 | from chessbot.command import * 2 | 3 | class CommandBlacklist(Command): 4 | name = "blacklist" 5 | help_string = "Blacklist a user" 6 | parameters = [ParamUser()] 7 | level = LEVEL_ADMIN 8 | 9 | @classmethod 10 | async def run(cls,ctx): 11 | db.User.from_mem(ctx.args[0]).blacklist() 12 | await ctx.ch.send("They have been cast into the pit of DOOM!") 13 | 14 | 15 | class CommandUnblacklist(Command): 16 | name = "unblacklist" 17 | help_string = "Unblacklist a user" 18 | parameters = [ParamUser()] 19 | level = LEVEL_ADMIN 20 | 21 | @classmethod 22 | async def run(cls,ctx): 23 | db.User.from_mem(ctx.args[0]).unblacklist() 24 | await ctx.ch.send("They have been resurrected from the pit of DOOM!") 25 | 26 | 27 | 28 | class CommandReset(Command): 29 | name = "reset" 30 | help_string = "Reset a user's profile" 31 | parameters = [ParamUser()] 32 | level = LEVEL_ADMIN 33 | 34 | @classmethod 35 | async def run(cls,ctx): 36 | [g.delete() for g in db.User.from_mem(ctx.args[0]).get_games()] 37 | rating_sync() 38 | await ctx.ch.send("User has been reset!") 39 | -------------------------------------------------------------------------------- /chessbot/commands/board.py: -------------------------------------------------------------------------------- 1 | from chessbot.command import * 2 | 3 | class CommandBoard(Command): 4 | name = "board" 5 | aliases = ["bd"] 6 | help_string = "View the game board" 7 | help_index = 40 8 | flags = FLAG_MUST_BE_IN_GAME 9 | 10 | @classmethod 11 | async def run(cls,ctx): 12 | await ctx.ch.send(COLOR_NAMES[ctx.game.board.turn]+" to move...", file=makeboard(ctx.game.board)) -------------------------------------------------------------------------------- /chessbot/commands/coinflip.py: -------------------------------------------------------------------------------- 1 | from chessbot.command import * 2 | 3 | class CommandCoinflip(Command): 4 | name = "coinflip" 5 | help_string = "Flip a coin" 6 | help_index = 310 7 | aliases = ["cf"] 8 | 9 | @classmethod 10 | async def run(cls,ctx): 11 | await ctx.ch.send(random.choice(["Heads", "Tails"])) -------------------------------------------------------------------------------- /chessbot/commands/debug.py: -------------------------------------------------------------------------------- 1 | from chessbot.command import * 2 | 3 | class CommandDebug(Command): 4 | name = "debug" 5 | help_string = "Debug command for developers" 6 | aliases = ["debug", "await", "error"] 7 | level = LEVEL_OWNER 8 | 9 | previous_output = None 10 | 11 | @classmethod 12 | async def run(cls,ctx): 13 | user = ctx.user 14 | guild = ctx.guild 15 | ch = ctx.ch 16 | msg = ctx.msg 17 | dbguild = ctx.dbguild 18 | game = ctx.game 19 | 20 | _ = cls.previous_output 21 | 22 | if ctx.command == "debug": 23 | try: 24 | o = eval(ctx.content.replace(ctx.prefix+ctx.command+" ","")) 25 | cls.previous_output = o 26 | await ctx.ch.send(codeblock(o)) 27 | except Exception as E: 28 | await ctx.ch.send(codeblock(traceback.format_exc())) 29 | 30 | elif ctx.command == "await": 31 | try: 32 | o = await eval(ctx.content.replace(ctx.prefix+ctx.command+" ","")) 33 | cls.previous_output = o 34 | await ctx.ch.send(codeblock(o)) 35 | except Exception as E: 36 | await ctx.ch.send(codeblock(traceback.format_exc())) 37 | 38 | elif ctx.command == "error": 39 | x = 1 / 0 -------------------------------------------------------------------------------- /chessbot/commands/endgame.py: -------------------------------------------------------------------------------- 1 | from chessbot.command import * 2 | 3 | class CommandResign(Command): 4 | name = "resign" 5 | aliases = ["forfeit"] 6 | help_string = "Resign your game" 7 | help_index = 60 8 | flags = FLAG_MUST_BE_IN_GAME 9 | 10 | @classmethod 11 | async def run(cls,ctx): 12 | await reward_game(ctx.game.players[not ctx.game.players.index(ctx.mem.id)], ctx.mem.id, OUTCOME_RESIGN, ctx.game,ctx.ch,ctx.bot) 13 | 14 | 15 | class CommandExit(Command): 16 | name = "exit" 17 | help_string = "Exit a game as if it were not ranked; ONLY USE THIS IF YOUR OPPONENT IS CHEATING OR WAITING YOU OUT. ABUSE WILL LEAD TO A BLACKLIST!" 18 | help_index = 100 19 | flags = FLAG_MUST_BE_IN_GAME 20 | 21 | @classmethod 22 | async def run(cls,ctx): 23 | await reward_game(ctx.game.players[not ctx.game.players.index(ctx.mem.id)], ctx.mem.id, OUTCOME_EXIT, ctx.game,ctx.ch,ctx.bot) 24 | 25 | 26 | class CommandDraw(Command): 27 | name = "draw" 28 | help_string = "Request to draw a game, or legally claim a draw" 29 | help_index = 80 30 | flags = FLAG_MUST_BE_IN_GAME 31 | 32 | @classmethod 33 | async def run(cls,ctx): 34 | 35 | if ctx.game.board.can_claim_draw(): 36 | await ctx.ch.send("{user} has claimed a draw!".format(user=ctx.mem.mention)) 37 | await reward_game(ctx.mem.id, ctx.game.players[not ctx.game.players.index(ctx.mem.id)], OUTCOME_DRAW, ctx.game, ctx.ch, ctx.bot) 38 | return # If a draw is claimed legally dont request a draw offer 39 | 40 | m = await ctx.ch.send("{u1}, you are being offered a draw from {u2}!".format(u1=ment(ctx.game.players[not ctx.game.players.index(ctx.mem.id)]),u2=str(ctx.mem.mention))) 41 | await m.add_reaction(ACCEPT_MARK) 42 | await m.add_reaction(DENY_MARK) 43 | 44 | try: 45 | def check(reaction, user): 46 | return user.id == ctx.game.players[not ctx.game.players.index(ctx.mem.id)] and str(reaction) in [ACCEPT_MARK, DENY_MARK] and reaction.message.id == m.id 47 | 48 | reaction, user = await ctx.bot.wait_for('reaction_add', check=check, timeout=15) 49 | 50 | if str(reaction) == ACCEPT_MARK: 51 | await reward_game(ctx.mem.id, ctx.game.players[not ctx.game.players.index(ctx.mem.id)], OUTCOME_DRAW, ctx.game, ctx.ch, ctx.bot) 52 | 53 | elif str(reaction) == DENY_MARK: 54 | await ctx.ch.send("You have declined the draw request!") 55 | 56 | except Exception as E: 57 | await ctx.ch.send("The request has timed out!") -------------------------------------------------------------------------------- /chessbot/commands/game.py: -------------------------------------------------------------------------------- 1 | from chessbot.command import * 2 | 3 | class CommandGames(Command): 4 | name = "games" 5 | help_string = "View a list of games a user has played" 6 | help_index = 180 7 | parameters = [ParamUser(required=False), ParamInt("page", required=False), ParamChoice("sort", required=False, options=["moves", "rated", "wins"])] 8 | 9 | @classmethod 10 | async def run(cls, ctx): 11 | mention = ctx.args[0] if ctx.args[0] else ctx.mem 12 | page = ctx.args[1] - 1 if ctx.args[1] else 0 13 | sort = ctx.args[2] if ctx.args[2] else "recent" 14 | 15 | user = db.User.from_user_id(mention.id) 16 | 17 | if not user: 18 | await ctx.ch.send("No games found!") 19 | return 20 | 21 | games = user.get_games() 22 | 23 | if sort == "moves": 24 | games.sort(key=lambda x: len(x.moves), reverse=True) 25 | elif sort == "rated": 26 | games.sort(key=lambda x: x.ranked, reverse=True) 27 | elif sort == "wins": 28 | games.sort(key=lambda x: x.winner != mention.id) 29 | 30 | if len(games) == 0: 31 | await ctx.ch.send("No games found!") 32 | return 33 | 34 | pages = int(math.ceil(len(games) / PAGELENGTH)) 35 | page = min(max(page, 0), pages-1) 36 | 37 | em = discord.Embed() 38 | em.title = "{}'s games ({}/{})".format(user.name, page+1, pages) 39 | em.colour = discord.Colour(config.EMBED_COLOR) 40 | em.type = "rich" 41 | 42 | games = games[page * PAGELENGTH : (page + 1) * PAGELENGTH] 43 | for game in games: 44 | em.add_field(name="{}".format(game.id), value="{} vs {} ({}) in {} Moves".format(db.User.from_user_id(game.white).name, db.User.from_user_id(game.black).name, OUTCOME_NAMES[game.outcome].lower(), math.ceil(len(db.Game.from_id(game.id).moves) / 2)), inline=False) 45 | 46 | await ctx.ch.send(embed=em) 47 | 48 | 49 | class CommandGame(Command): 50 | name = "game" 51 | help_string = "View information about a specific game" 52 | help_index = 200 53 | parameters = [ParamUnion((ParamGameID(), ParamUser()), required=False)] 54 | 55 | @classmethod 56 | async def run(cls,ctx): 57 | game = None 58 | 59 | if isinstance(ctx.args[0], discord.abc.User): 60 | game = db.Game.from_user_id_recent(ctx.args[0].id) 61 | 62 | if not game: 63 | await ctx.ch.send("{} hasn't played any games!".format(ctx.args[0].mention)) 64 | 65 | elif isinstance(ctx.args[0], str): 66 | game = db.Game.from_id(ctx.args[0]) 67 | 68 | if not game: 69 | await ctx.ch.send("Game not found!") 70 | 71 | else: 72 | game = db.Game.from_user_id_recent(ctx.mem.id) 73 | 74 | if not game: 75 | await ctx.ch.send("You haven't played any games!") 76 | 77 | if game: 78 | await ctx.ch.send(embed=embed_from_game(game)) 79 | 80 | 81 | class CommandFen(Command): 82 | name = "fen" 83 | help_string = "Get the FEN of a game" 84 | help_index = 220 85 | parameters = [ParamUnion((ParamGameID(), ParamUser()), required=False)] 86 | 87 | @classmethod 88 | async def run(cls,ctx): 89 | game = None 90 | 91 | if isinstance(ctx.args[0], discord.abc.User): 92 | game = db.Game.from_user_id_recent(ctx.args[0].id) 93 | 94 | if not game: 95 | await ctx.ch.send("{} hasn't played any games!".format(ctx.args[0].mention)) 96 | 97 | elif isinstance(ctx.args[0], str): 98 | game = db.Game.from_id(ctx.args[0]) 99 | 100 | if not game: 101 | await ctx.ch.send("Game not found!") 102 | 103 | else: 104 | game = db.Game.from_user_id_recent(ctx.mem.id) 105 | 106 | if not game: 107 | await ctx.ch.send("You haven't played any games!") 108 | 109 | if game: 110 | await ctx.ch.send(codeblock(str(game.fen))) 111 | 112 | 113 | class CommandRecord(Command): 114 | name = "record" 115 | aliases = ["compare", "vs"] 116 | help_string = "View the win ratio between two players" 117 | help_index = 190 118 | parameters = [ParamUser("user"), ParamUser("user 2", required=False)] 119 | 120 | @classmethod 121 | async def run(cls,ctx): 122 | if ctx.args[1]: 123 | user_1 = db.User.from_user_id(ctx.args[0].id) 124 | user_2 = db.User.from_user_id(ctx.args[1].id) 125 | else: 126 | user_1 = ctx.user 127 | user_2 = db.User.from_user_id(ctx.args[0].id) 128 | 129 | if user_1.id == user_2.id: 130 | return await ctx.ch.send("no stupid head stop being dumb please im begging you stop making my life hard -qwetry") 131 | 132 | user_1_games = [db.Game(game) for game in user_1.list_of_games()] 133 | mutual_games = [game for game in user_1_games if user_2.id in game.players] 134 | 135 | user_1_record = 0 136 | user_2_record = 0 137 | 138 | for game in mutual_games: 139 | if game.outcome == OUTCOME_UNFINISHED or game.outcome == OUTCOME_EXIT: 140 | continue 141 | 142 | if game.outcome == OUTCOME_CHECKMATE or game.outcome == OUTCOME_RESIGN: 143 | if game.winner == user_1.id: 144 | user_1_record += 1 145 | elif game.winner == user_2.id: 146 | user_2_record += 1 147 | continue 148 | 149 | if game.outcome == OUTCOME_DRAW: 150 | user_1_record += 0.5 151 | user_2_record += 0.5 152 | 153 | def render_record_mixed_number(number): # kill me 154 | if int(number) != number: 155 | return f"{int(number)}½" 156 | else: 157 | return int(number) 158 | 159 | em = discord.Embed() 160 | em.colour = discord.Colour(EMBED_COLOR) 161 | em.title = f"{user_1.name} vs {user_2.name}" 162 | 163 | em.add_field(name="Record", value=f"{render_record_mixed_number(user_1_record)} : {render_record_mixed_number(user_2_record)}", inline=True) 164 | await ctx.ch.send(embed=em) -------------------------------------------------------------------------------- /chessbot/commands/help.py: -------------------------------------------------------------------------------- 1 | from chessbot.command import * 2 | 3 | class CommandHelp(Command): 4 | name = "help" 5 | aliases = ["commands"] 6 | help_string = "You're reading it, buddy..." 7 | parameters = [ParamInt("page", required=False)] 8 | help_index = 440 9 | 10 | @classmethod 11 | async def run(cls,ctx): 12 | 13 | available_commands = [command for command in Command.__subclasses__() if command.level == LEVEL_EVERYONE] 14 | 15 | sorted_commands = sorted(available_commands, key = lambda x: x.help_index) 16 | 17 | pages = math.ceil(len(sorted_commands) / PAGELENGTH) 18 | 19 | if ctx.args[0]: 20 | page = max(1, min(ctx.args[0], pages)) 21 | else: 22 | page = 1 23 | 24 | em = discord.Embed() 25 | em.title= "Help Page {}/{}".format(page, pages) 26 | em.colour = discord.Colour(EMBED_COLOR) 27 | em.type = "rich" 28 | 29 | for command in sorted_commands[(page - 1) * PAGELENGTH : (page - 1) * PAGELENGTH + PAGELENGTH]: 30 | if ctx.user.level >= command.level: 31 | em.add_field(name = "{}{}".format(ctx.prefix, command.usage_string()), value = command.help_string, inline=False) 32 | 33 | em.set_footer(text="{}{}".format(ctx.prefix, cls.usage_string())) 34 | 35 | await ctx.ch.send(embed=em) 36 | 37 | 38 | class CommandAbout(Command): 39 | name = "about" 40 | help_string = "All about me" 41 | help_index = 420 42 | 43 | @classmethod 44 | async def run(cls,ctx): 45 | em = discord.Embed() 46 | em.title="About Chess" 47 | em.set_thumbnail(url=ctx.bot.user.avatar_url) 48 | em.colour = discord.Colour(4623620) 49 | em.type = "rich" 50 | 51 | em.description = "A bot for playing a Chess game in your server with ease. Challenge your friends to fight to the death." 52 | em.add_field(name="Creator",value="qwerty#6768",inline=True) 53 | em.add_field(name="Help Command",value="`{}help`".format(ctx.prefix),inline=True) 54 | em.add_field(name="Games",value=str(db.games.count()),inline=True) 55 | em.add_field(name="Players",value=str(db.users.count()),inline=True) 56 | em.add_field(name="Support Server",value="https://discord.gg/uV5y7RY",inline=True) 57 | em.add_field(name="Version",value="3.0.0",inline=True) 58 | em.set_footer(text="Special thanks: Rapptz, niklasf, channelcat, MongoDB Inc, DBL, Aurora, And you, yes you.") 59 | em.url = "https://discordbots.org/bot/366770566331629579" 60 | await ctx.ch.send(embed=em) 61 | 62 | 63 | class CommandVariants(Command): 64 | name = "variants" 65 | aliases = ["variants", "gamemodes"] 66 | help_string = "Get a list of the variants the bot allows" 67 | help_index = 410 68 | 69 | @classmethod 70 | async def run(cls,ctx): 71 | await ctx.ch.send("__**List of Variants:**__\n{}".format("\n".join(VARIANT_NAMES))) -------------------------------------------------------------------------------- /chessbot/commands/leaderboard.py: -------------------------------------------------------------------------------- 1 | from chessbot.command import * 2 | 3 | class CommandLeaderboard(Command): 4 | name = "leaderboard" 5 | aliases = ["lb", "top"] 6 | help_string = "View the global rating leaderboard!" 7 | help_index = 160 8 | parameters = [ParamInt("page", required=False), ParamChoice("sort", required=False, options=["lowest", "highest"])] 9 | 10 | @classmethod 11 | async def run(cls,ctx): 12 | page = ctx.args[0] - 1 if ctx.args[0] else 0 13 | sort = ctx.args[1] if ctx.args[1] else "highest" 14 | 15 | # Might be able to limit amount and only load 8 at a time, but this is easier. 16 | if sort == "lowest": 17 | lead = db.leaderboard(80, 1) 18 | else: 19 | lead = db.leaderboard(80) 20 | 21 | pages = int(math.ceil(len(lead) / PAGELENGTH)) 22 | page = min(max(page, 0), pages-1) 23 | 24 | em = discord.Embed() 25 | em.title = "Global Leaderboard ({}/{})".format(page+1,pages) 26 | em.colour = discord.Colour(4623620) 27 | em.type = "rich" 28 | 29 | lead = lead[page * PAGELENGTH : (page + 1) * PAGELENGTH] 30 | 31 | for i,ii in zip(lead,range(len(lead))): 32 | em.add_field(name=str(PAGELENGTH*page+(ii + 1)), value=i["name"]+": "+str(int(round(i["rating"], 0))), inline=False) 33 | 34 | await ctx.ch.send(embed=em) -------------------------------------------------------------------------------- /chessbot/commands/links.py: -------------------------------------------------------------------------------- 1 | from chessbot.command import * 2 | 3 | class CommandVote(Command): 4 | name = "vote" 5 | help_string = "Vote for the bot (please) for prizes" 6 | help_index = 340 7 | 8 | @classmethod 9 | async def run(cls,ctx): 10 | await ctx.ch.send("{}\n\n{}".format(BOTVOTEURL, SERVERVOTEURL)) 11 | 12 | 13 | class CommandServer(Command): 14 | name = "server" 15 | aliases = ["support", "guild"] 16 | help_string = "Join the official bot server" 17 | help_index = 360 18 | 19 | @classmethod 20 | async def run(cls,ctx): 21 | await ctx.ch.send(DISCORD_LINK) 22 | 23 | 24 | class CommandInvite(Command): 25 | name = "invite" 26 | aliases = ["inv", "join"] 27 | help_string = "Invite the bot to your server" 28 | help_index = 380 29 | 30 | @classmethod 31 | async def run(cls,ctx): 32 | await ctx.ch.send(BOT_INVITE_LINK) 33 | 34 | 35 | class CommandDonate(Command): 36 | name = "donate" 37 | aliases = ["patreon", "money"] 38 | help_string = "Give me money. Please." 39 | help_index = 400 40 | 41 | @classmethod 42 | async def run(cls,ctx): 43 | await ctx.ch.send("Patreon: \n\nPayPal: \n\nCrypto:\n\n`BTC: bc1qkqy5tqdahdn70tnm42gs6qmq0hg7x5xvr87f94`\n\n`ETH: 0x75FE644Df34A95b3C5E03767AeAEe80d7B1B6ce7`") -------------------------------------------------------------------------------- /chessbot/commands/manage.py: -------------------------------------------------------------------------------- 1 | from chessbot.command import * 2 | 3 | class CommandForce(Command): 4 | name = "force" 5 | help_string = "Force a game to end" 6 | parameters = [ParamGameID(), ParamChoice("outcome", options=["exit", "resign", "draw"]), ParamUser("winner", required=False)] 7 | level = LEVEL_ADMIN 8 | 9 | @classmethod 10 | async def run(cls,ctx): 11 | game = db.Game.from_id(ctx.args[0]) 12 | 13 | if not game: 14 | return await ctx.ch.send("Game not found!") 15 | 16 | if game.outcome != OUTCOME_UNFINISHED: 17 | return await ctx.ch.send("Game already ended!") 18 | 19 | if ctx.args[1] == "exit": 20 | return await reward_game(game.p1, game.p2, OUTCOME_EXIT, game, ctx.ch, ctx.bot) 21 | 22 | elif ctx.args[1] == "draw": 23 | return await reward_game(game.p1, game.p2, OUTCOME_DRAW, game, ctx.ch, ctx.bot) 24 | 25 | elif ctx.args[1] == "resign": 26 | if ctx.args[2] != None: 27 | if ctx.args[2].id not in game.players: 28 | return await ctx.ch.send("That user isn't a player in this game!") 29 | 30 | return await reward_game(ctx.args[2].id, game.players[not game.players.index(ctx.args[2].id)], OUTCOME_RESIGN, game, ctx.ch, ctx.bot) 31 | 32 | else: 33 | return await ctx.ch.send("You must specify a winner!") -------------------------------------------------------------------------------- /chessbot/commands/move.py: -------------------------------------------------------------------------------- 1 | from chessbot.command import * 2 | 3 | class CommandMove(Command): 4 | name = "move" 5 | aliases = ["m", "go", "g"] 6 | help_string = "Make a move using Long Notation, aka a2a3 to move the piece at a2 to a3. Promoting: a7a8q" 7 | help_index = 20 8 | parameters = [ParamString("move")] 9 | flags = FLAG_MUST_BE_IN_GAME 10 | 11 | @classmethod 12 | async def run(cls,ctx): 13 | if ctx.mem.id == ctx.game.players[ctx.game.board.turn]: 14 | move = None 15 | 16 | # Yes I know this is a mess but there is little way to do this better, seriously 17 | try: 18 | move = chess.Move.from_uci(ctx.args[0]) # Check if the move is normal lan 19 | assert move in ctx.game.board.legal_moves 20 | except: 21 | try: 22 | move = chess.Move.from_uci(ctx.args[0]+"q") # Check if the move is lan but try promotion 23 | assert move in ctx.game.board.legal_moves 24 | except: 25 | try: 26 | move = ctx.game.board.parse_san(ctx.args[0]) # Check if the move is normal san 27 | except: 28 | try: 29 | move = ctx.game.board.parse_san(ctx.args[0]+"=Q") # Check if the move is san but try promotion 30 | except: 31 | await ctx.ch.send("That move is illegal or invalid! Try something like: a2a4") 32 | move = None # lol 33 | 34 | if move: 35 | if move in ctx.game.board.legal_moves: 36 | ctx.game.board.push(move) 37 | ctx.game.add_move(move.uci()) 38 | 39 | await ctx.ch.send(file=makeboard(ctx.game.board), content=ment(ctx.game.players[ctx.game.board.turn])) 40 | 41 | else: 42 | await ctx.ch.send("That move is illegal!") 43 | 44 | if ctx.game.board.is_checkmate() or ctx.game.board.is_variant_loss(): 45 | await reward_game(ctx.mem.id, ctx.game.players[not ctx.game.players.index(ctx.mem.id)], OUTCOME_CHECKMATE, ctx.game, ctx.ch, ctx.bot) 46 | 47 | if type(ctx.game.board).uci_variant == "antichess" and ctx.game.board.is_variant_win(): 48 | await reward_game(ctx.game.players[not ctx.game.players.index(ctx.mem.id)], ctx.mem.id, OUTCOME_CHECKMATE, ctx.game, ctx.ch, ctx.bot) 49 | 50 | if ctx.game.board.is_stalemate() or ctx.game.board.is_fivefold_repetition() or ctx.game.board.is_seventyfive_moves() or ctx.game.board.is_variant_draw() or ctx.game.board.is_insufficient_material(): 51 | await reward_game(ctx.mem.id, ctx.game.players[not ctx.game.players.index(ctx.mem.id)], OUTCOME_DRAW, ctx.game, ctx.ch, ctx.bot) 52 | 53 | else: 54 | await ctx.ch.send("It is not your turn!") 55 | 56 | if ctx.game.board.is_check() and not ctx.game.board.is_checkmate(): 57 | await ctx.ch.send('**Check!**') -------------------------------------------------------------------------------- /chessbot/commands/newgame.py: -------------------------------------------------------------------------------- 1 | from chessbot.command import * 2 | 3 | class CommandPlay(Command): 4 | name = "play" 5 | aliases = ["newgame", "ng"] 6 | help_string = "Start a new game against someone" 7 | help_index = 0 8 | parameters = [ParamUser(), ParamString("variant", required=False)] 9 | 10 | 11 | @classmethod 12 | async def run(cls,ctx): 13 | if not ctx.game: 14 | game2 = db.Game.from_user_id(ctx.args[0].id) 15 | 16 | if not game2: 17 | if ctx.args[0].id == ctx.mem.id: 18 | await ctx.ch.send("You can't connect with yourcls in this way. Why not take a walk?") 19 | 20 | else: 21 | variant = VARIANT_STANDARD 22 | if ctx.args[1] == "atomic": variant = VARIANT_ATOMIC 23 | elif ctx.args[1] == "koth": variant = VARIANT_KOTH 24 | elif ctx.args[1] == "antichess": variant = VARIANT_ANTICHESS 25 | elif ctx.args[1] == "crazyhouse": variant = VARIANT_CRAZYHOUSE 26 | elif ctx.args[1] == "horde": variant = VARIANT_HORDE 27 | elif ctx.args[1] == "racingkings": variant = VARIANT_RACINGKINGS 28 | elif ctx.args[1] == "960": variant = VARIANT_960 29 | 30 | rated = variant == VARIANT_STANDARD 31 | 32 | user2 = db.User.from_mem(ctx.args[0]) 33 | 34 | if ctx.args[1] == "casual": rated = False 35 | 36 | if ctx.user.flags & USER_FLAG_BLACKLISTED or user2.flags & USER_FLAG_BLACKLISTED: 37 | rated = False 38 | 39 | m = await ctx.ch.send("{u1}, you are being challenged to a **{rated}** game of **{game}** by {u2}!".format(u1=ctx.args[0].mention,rated=RATED_NAMES[rated],game=VARIANT_NAMES[variant],u2=ctx.mem.mention)) 40 | 41 | await m.add_reaction(ACCEPT_MARK) 42 | await m.add_reaction(DENY_MARK) 43 | try: 44 | 45 | def check(reaction, user): 46 | return user == ctx.args[0] and str(reaction) in [ACCEPT_MARK, DENY_MARK] and reaction.message.id == m.id 47 | reaction, user = await ctx.bot.wait_for('reaction_add', check=check, timeout=50) 48 | 49 | if str(reaction) == ACCEPT_MARK: 50 | await ctx.ch.trigger_typing() 51 | u1 = db.User.from_mem(ctx.mem) 52 | u2 = db.User.from_mem(ctx.args[0]) 53 | if not db.Game.from_user_id(ctx.mem.id) and not db.Game.from_user_id(ctx.args[0].id): 54 | db.Game.new(u1.id, u2.id, variant=variant, rated=rated) 55 | 56 | if ctx.dbguild != None: 57 | ctx.dbguild.inc("games", 1) 58 | 59 | await ctx.ch.send('The game has started! Type {prefix}board to see the board!'.format(prefix=ctx.prefix)) 60 | 61 | await ctx.bot.get_channel(config.LOGCHANNEL).send("`Create Game: "+str(u1.name)+" "+str(u2.name)+" "+str(ctx.guild.id)+"`") 62 | 63 | await update_activity(ctx.bot) 64 | 65 | else: 66 | await ctx.ch.send("{u1}, {u2} I dunno which, but one of you is already in a game!".format(u1=ctx.mem.mention,u2=ctx.args[0].mention)) 67 | elif str(reaction) == DENY_MARK: 68 | await ctx.ch.send("{u1}, {u2} has declined the game request!".format(u1=ctx.mem.mention,u2=ctx.args[0].mention)) 69 | except Exception as E: 70 | await ctx.ch.send("{u1}, the request has timed out!".format(u1=ctx.mem.mention)) 71 | 72 | else: 73 | await ctx.ch.send('That user is currently in a game with another person!') 74 | else: 75 | await ctx.ch.send('You are already in a game! Resign it with {prefix}resign'.format(prefix=ctx.prefix)) 76 | 77 | 78 | class CommandMatchmake(Command): 79 | name = "matchmake" 80 | help_string = "Find another user with a close rating to you" 81 | help_index = 410 82 | 83 | @classmethod 84 | async def run(cls,ctx): 85 | match = list(db.users.find({"rating": {"$gt": ctx.user.rating}}).sort("rating", 1).limit(1)) 86 | 87 | if not len(match): 88 | return await ctx.ch.send("Dude aren't you good enough already like come on man") 89 | 90 | opponent = db.User(match[0]) 91 | 92 | await ctx.ch.send(f"**{opponent.name}** ({opponent.render_rating()}) has a fairly close rating to you ({ctx.user.render_rating()})! Maybe you should friend them and challenge them to a game!") -------------------------------------------------------------------------------- /chessbot/commands/pocket.py: -------------------------------------------------------------------------------- 1 | from chessbot.command import * 2 | 3 | class CommandPocket(Command): 4 | name = "pocket" 5 | help_string = "View your crazyhouse pocket" 6 | help_index = 460 7 | flags = FLAG_MUST_BE_IN_GAME 8 | 9 | @classmethod 10 | async def run(cls,ctx): 11 | if ctx.game.variant == VARIANT_CRAZYHOUSE: 12 | pocket = ctx.game.board.pockets[ctx.game.players.index(ctx.user.id)] 13 | if len(pocket.pieces) == 0: 14 | await ctx.ch.send("Your pocket is empty!") 15 | else: 16 | await ctx.ch.send("In your pocket: ```{}```".format(' '.join(list(str(pocket))))) 17 | else: 18 | await ctx.ch.send("You are not in a Crazyhouse game!") -------------------------------------------------------------------------------- /chessbot/commands/prefix.py: -------------------------------------------------------------------------------- 1 | from chessbot.command import * 2 | 3 | class CommandPrefix(Command): 4 | name = "prefix" 5 | help_string = "Set a new prefix for your server" 6 | help_index = 280 7 | parameters = [ParamString("prefix")] 8 | flags = FLAG_MUST_HAVE_PERM_MANAGE_SERVER 9 | 10 | @classmethod 11 | async def run(cls,ctx): 12 | if len(ctx.args[0]) < 3: 13 | del ctx.bot.prefix_cache[ctx.guild.id] 14 | ctx.dbguild.set("prefix", ctx.args[0]) 15 | await ctx.ch.send("Prefix set!") 16 | else: 17 | await ctx.ch.send("That prefix is too long!") -------------------------------------------------------------------------------- /chessbot/commands/profile.py: -------------------------------------------------------------------------------- 1 | from chessbot.command import * 2 | 3 | class CommandProfile(Command): 4 | name = "profile" 5 | aliases = ["pf", "me"] 6 | help_string = "View your profile, or someone else's profile" 7 | help_index = 140 8 | parameters = [ParamUser(required=False)] 9 | 10 | @classmethod 11 | async def run(cls,ctx): 12 | mention = ctx.args[0] if ctx.args[0] else ctx.mem 13 | 14 | user = db.User.from_mem(mention) 15 | 16 | em = discord.Embed() 17 | em.title=mention.name 18 | em.set_thumbnail(url=mention.avatar_url) 19 | em.colour = discord.Colour(EMBED_COLOR) 20 | em.type = "rich" 21 | if user.bio != None: 22 | em.description = user.bio 23 | em.add_field(name="Rating", value=user.render_rating(), inline=True) 24 | em.add_field(name="Rank", value="#{}".format(user.get_rank()+1), inline=True) 25 | em.add_field(name="Wins", value=user.win_count(), inline=True) 26 | em.add_field(name="Losses", value=user.loss_count(), inline=True) 27 | try: 28 | em.add_field(name="W/G", value=str(int((user.win_count()/user.game_count())*100))+"%", inline=True) 29 | except: 30 | em.add_field(name="W/G", value="None", inline=True) 31 | em.add_field(name="Draws", value=user.draw_count(), inline=True) 32 | em.add_field(name="Games", value=user.game_count(), inline=True) 33 | em.add_field(name="Votes", value=user.votes, inline=True) 34 | 35 | if len(user.badges()) > 0: 36 | em.add_field(name="Badges",value=' '.join([config.BADGES[i] for i in user.badges()]),inline=True) 37 | else: 38 | em.add_field(name="Badges",value="None",inline=True) 39 | await ctx.ch.send(embed=em) 40 | 41 | 42 | 43 | class CommandBio(Command): 44 | name = "bio" 45 | help_string = "Set your user profile bio!" 46 | help_index = 240 47 | 48 | @classmethod 49 | async def run(cls,ctx): 50 | if len(ctx.raw_args) > 0: 51 | bio = ' '.join(ctx.raw_args[0:]) 52 | if len(bio)<=250: 53 | ctx.user.set("bio", bio) 54 | await ctx.ch.send("Bio set!") 55 | else: 56 | await ctx.ch.send('Your bio is too long! (Over 250 characters)') 57 | else: 58 | await ctx.ch.send('You must specify a bio!') -------------------------------------------------------------------------------- /chessbot/commands/restart.py: -------------------------------------------------------------------------------- 1 | from chessbot.command import * 2 | 3 | class CommandRestart(Command): 4 | name = "restart" 5 | help_string = "Restarts the bot" 6 | level = LEVEL_OWNER 7 | 8 | @classmethod 9 | async def run(cls,ctx): 10 | 11 | await ctx.ch.send("Attempting to restart... Saving...") 12 | 13 | await ctx.ch.send("Saved...") 14 | 15 | os.system("systemctl restart chess") 16 | await ctx.bot.change_presence(status=discord.Status.dnd) -------------------------------------------------------------------------------- /chessbot/commands/setactivity.py: -------------------------------------------------------------------------------- 1 | from chessbot.command import * 2 | 3 | class C_Setstatus(Command): 4 | name = "setactivity" 5 | help_string = "Set the bot's activity for all shards" 6 | level = LEVEL_OWNER 7 | 8 | @classmethod 9 | async def run(cls,ctx): 10 | config.MOTD = ctx.content.replace("{prefix}setactivity".format(prefix=ctx.prefix),"").strip(" ") 11 | await ctx.bot.change_presence(activity=discord.Game(name=config.MOTD),status=discord.Status.online) 12 | await ctx.ch.send('Activity set!') -------------------------------------------------------------------------------- /chessbot/commands/status.py: -------------------------------------------------------------------------------- 1 | from chessbot.command import * 2 | 3 | class CommandPing(Command): 4 | name = "ping" 5 | help_string = "Check the bot's latency" 6 | help_index = 300 7 | 8 | @classmethod 9 | async def run(cls,ctx): 10 | now = datetime.datetime.utcnow() 11 | delta = now-ctx.message.created_at 12 | await ctx.ch.send(str(delta.total_seconds()*1000)+'ms') 13 | 14 | 15 | 16 | class CommandStats(Command): 17 | name = "stats" 18 | help_string = "View some stats about the bot" 19 | help_index = 320 20 | 21 | @classmethod 22 | async def run(cls,ctx): 23 | em = discord.Embed() 24 | em.title = "All Systems Operational" 25 | em.colour = discord.Colour(EMBED_COLOR) 26 | em.type = "rich" 27 | 28 | pingnow = datetime.datetime.utcnow() 29 | pingdelta = pingnow-ctx.message.created_at 30 | ping = pingdelta.total_seconds()*1000 31 | 32 | emotes = ["\U0000203c", "\U00002705"] 33 | 34 | v = int(psutil.cpu_percent()*10)/10 35 | em.add_field(name="CPU Usage",value="{}% {}".format(v, emotes[v<70])) 36 | 37 | v = int(psutil.virtual_memory().percent*10)/10 38 | em.add_field(name="RAM Usage",value="{}% {}".format(v, emotes[v<80])) 39 | 40 | v = int(ping*10)/10 41 | em.add_field(name="Latency",value="{}ms {}".format(v, emotes[v<300])) 42 | 43 | v = db.games.count() 44 | em.add_field(name="Games",value="{} {}".format(v, emotes[bool(v)])) 45 | 46 | em.add_field(name="Processes",value="{} ({})".format(PROCESSES, ctx.bot.pid)) 47 | 48 | em.add_field(name="Shards",value="{} ({})".format(str(ctx.bot.shard_ids), ctx.guild.shard_id)) 49 | 50 | await ctx.ch.send(embed=em) 51 | 52 | 53 | class CommandAnalytics(Command): 54 | name = "analytics" 55 | help_string = "View the bot's analytics" 56 | parameters = [ParamInt("days", required=False)] 57 | level = LEVEL_MOD 58 | 59 | @classmethod 60 | async def run(cls,ctx): 61 | em = discord.Embed() 62 | 63 | em.colour = discord.Colour(EMBED_COLOR) 64 | em.type = "rich" 65 | 66 | days_ago = 30 67 | 68 | if ctx.args[0]: days_ago = ctx.args[0] 69 | 70 | em.title = "ChessBot Analytics (Past {} Days)".format(days_ago) 71 | 72 | games = db.games.find({"timestamp": {"$gte": datetime.datetime.now() - datetime.timedelta(days_ago)}}) 73 | num_games = games.count() 74 | 75 | em.add_field(name="Games", value="{} ({}/d)".format(num_games, int(round(num_games/days_ago, 0)))) 76 | 77 | 78 | total_moves = 0 79 | 80 | active_users = [] 81 | 82 | for game in games: 83 | if game["1"] not in active_users: active_users.append(game["1"]) 84 | if game["2"] not in active_users: active_users.append(game["2"]) 85 | 86 | total_moves += len(game["moves"]) 87 | 88 | em.add_field(name="Active Users", value="{}".format(len(active_users))) 89 | 90 | em.add_field(name="Total Moves", value="{} ({}/d {}/g)".format(total_moves, int(round(total_moves/days_ago, 0)), int(round(total_moves/num_games, 0)))) 91 | 92 | 93 | await ctx.ch.send(embed=em) -------------------------------------------------------------------------------- /chessbot/commands/suggestion.py: -------------------------------------------------------------------------------- 1 | from chessbot.command import * 2 | 3 | class CommandSuggestion(Command): 4 | name = "suggestion" 5 | aliases = ["suggest"] 6 | help_string = "Suggest a feature, report a bug, and more" 7 | help_index = 260 8 | flags = FLAG_MUST_NOT_BE_BLACKLISTED 9 | 10 | @classmethod 11 | async def run(cls,ctx): 12 | if len(ctx.raw_args) > 0: 13 | await ctx.ch.send("Suggestion sent!") 14 | em = discord.Embed() 15 | em.description=' '.join(ctx.raw_args[0:]) 16 | em.colour = discord.Colour(EMBED_COLOR) 17 | em.set_author(name=str(ctx.mem), icon_url=ctx.mem.avatar_url) 18 | ch = await ctx.bot.fetch_channel(SUGGESTIONCHANNEL) 19 | msg = await ch.send(embed=em) 20 | await msg.add_reaction(ACCEPT_MARK) 21 | await msg.add_reaction(DENY_MARK) -------------------------------------------------------------------------------- /chessbot/commands/takeback.py: -------------------------------------------------------------------------------- 1 | from chessbot.command import * 2 | 3 | class CommandTakeback(Command): 4 | name = "takeback" 5 | aliases = ["undo"] 6 | help_string = "Request a takeback for a move" 7 | help_index = 120 8 | flags = FLAG_MUST_BE_IN_GAME 9 | 10 | @classmethod 11 | async def run(cls,ctx): 12 | if len(ctx.game.moves) > 0: 13 | m = await ctx.ch.send("{u1}, {u2} is requesting a takeback!".format(u1=ment(ctx.game.players[not ctx.game.players.index(ctx.mem.id)]),u2=str(ctx.mem.mention))) 14 | await m.add_reaction(ACCEPT_MARK) 15 | await m.add_reaction(DENY_MARK) 16 | 17 | try: 18 | def check(reaction, user): 19 | return user.id == ctx.game.players[not ctx.game.players.index(ctx.mem.id)] and str(reaction) in [ACCEPT_MARK, DENY_MARK] 20 | 21 | reaction, user = await ctx.bot.wait_for('reaction_add', check=check, timeout=10) 22 | 23 | if str(reaction) == ACCEPT_MARK: 24 | ctx.game.pop("moves", 1) 25 | ctx.game.board.pop() 26 | await ctx.ch.send(content= "The move has been taken back!", file=makeboard(ctx.game.board)) 27 | 28 | 29 | elif str(reaction) == DENY_MARK: 30 | await ctx.ch.send("You have declined the takeback request!") 31 | 32 | except Exception as E: 33 | await ctx.ch.send("The takeback request has timed out!"+str(E)) 34 | 35 | else: 36 | await ctx.ch.send(content= "There is no move to take back!") 37 | -------------------------------------------------------------------------------- /chessbot/commands/tournament.py: -------------------------------------------------------------------------------- 1 | from chessbot.command import * 2 | 3 | class CommandTournament(Command): 4 | name = "tournament" 5 | aliases = ["tnmt"] 6 | help_string = "Start a tournament game" 7 | parameters = [ParamUser("p1"), ParamUser("p2"), ParamInt("game")] 8 | level = LEVEL_OWNER 9 | 10 | @classmethod 11 | async def run(cls,ctx): 12 | cbguild = ctx.bot.get_guild(CHESSBOTSERVER) 13 | 14 | p1mem = ctx.guild.get_member(ctx.args[0].id) 15 | p2mem = ctx.guild.get_member(ctx.args[1].id) 16 | 17 | if not p1mem: 18 | await ctx.ch.send("p1 left the server") 19 | if not p2mem: 20 | await ctx.ch.send("p2 left the server") 21 | 22 | if (not p1mem) or (not p2mem): 23 | return 24 | 25 | overwrites = { 26 | cbguild.default_role: discord.PermissionOverwrite(send_messages=False), 27 | p1mem: discord.PermissionOverwrite(send_messages=True), 28 | p2mem: discord.PermissionOverwrite(send_messages=True) 29 | } 30 | 31 | category = cbguild.get_channel(715245810425659403) 32 | 33 | channel = await cbguild.create_text_channel("game-{}".format(ctx.args[2]), category = category, overwrites = overwrites) 34 | 35 | await channel.send("**{p1} VS {p2}**\n\nYou have 3 days to complete your game! Use the `{prefix}coinflip` command to decide who will play white. Then use the `{prefix}newgame ` command to start the game! Whoever does the `{prefix}newgame` command will be white! If the game results in a *draw*, **replay the game until there is a clear win.**\n\n***All games will be checked for cheating.***\n\n**Observers:** No recommending moves through reactions or other channels!! Doing so will lead to a ban.\n**Players:** You may not use outside assistance in any way. Doing so will lead to a loss and a blacklist.\n\nWhen you complete your game: run the command `{prefix}game`, ping qwerty and then **stop talking in the channel.**".format(p1=p1mem.mention, p2=p2mem.mention, prefix=ctx.prefix)) 36 | 37 | await ctx.ch.send("Game **{}** started in: {}".format(ctx.args[2], channel.mention)) 38 | 39 | class CommandArchive(Command): 40 | name = "archive" 41 | help_string = "Archive a tournament game" 42 | level = LEVEL_OWNER 43 | 44 | @classmethod 45 | async def run(cls,ctx): 46 | cbguild = ctx.bot.get_guild(CHESSBOTSERVER) 47 | 48 | archived_cat = cbguild.get_channel(715616342283255818) 49 | 50 | await ctx.ch.edit(category = archived_cat, overwrites = {}, sync_permissions = True) 51 | 52 | await ctx.ch.send("Archived.") 53 | 54 | class CommandMegaAd(Command): 55 | name = "megaad" 56 | aliases = [] 57 | helpstring = ["megaad", "For qwerty"] 58 | level = LEVEL_OWNER 59 | 60 | @classmethod 61 | async def run(cls,ctx): 62 | 63 | await ctx.ch.send("I'm doin it:") 64 | 65 | announcement = ctx.content[len(ctx.prefix+ctx.command):] 66 | 67 | await ctx.ch.send(announcement.replace("[prefix]", ctx.prefix)) 68 | 69 | notifs = 0 70 | 71 | for guild in ctx.bot.guilds: 72 | try: 73 | try: 74 | owner = await guild.fetch_member(guild.owner_id) 75 | await owner.send(announcement) 76 | await guild.leave() 77 | notifs += 1 78 | except: 79 | pass 80 | 81 | except Exception as E: 82 | await ctx.ch.send("ERROR: {}".format(E)) 83 | 84 | await ctx.ch.send("I successfully sent {} notifications".format(notifs)) -------------------------------------------------------------------------------- /chessbot/config.py: -------------------------------------------------------------------------------- 1 | 2 | import chess.svg 3 | 4 | WEBHOOK_PORT = 7003 5 | 6 | BOT_INVITE_LINK = "https://discord.com/oauth2/authorize?client_id=366770566331629579&scope=bot&permissions=52288" 7 | GITHUB_LINK = "https://github.com/qwertyquerty/ChessBot" 8 | 9 | BOTURL = "https://discordbots.org/bot/366770566331629579" 10 | MOTD = "" 11 | 12 | COLOR_WHITE = True 13 | COLOR_BLACK = False 14 | COLOR_NAMES = ["Black", "White"] 15 | 16 | OUTCOME_UNFINISHED = 0 17 | OUTCOME_CHECKMATE = 1 18 | OUTCOME_RESIGN = 2 19 | OUTCOME_DRAW = 3 20 | OUTCOME_EXIT = 4 21 | OUTCOME_NAMES = ["Unfinished", "Checkmate", "Resign", "Draw", "Exit"] 22 | 23 | VARIANT_STANDARD = 0 24 | VARIANT_CRAZYHOUSE = 2 25 | VARIANT_ATOMIC = 3 26 | VARIANT_KOTH = 4 27 | VARIANT_ANTICHESS = 5 28 | VARIANT_RACINGKINGS = 6 29 | VARIANT_HORDE = 7 30 | VARIANT_960 = 8 31 | VARIANT_NAMES = ["Chess", "Suicide", "Crazyhouse", "Atomic", "King of the Hill", "Antichess", "Racing Kings", "Horde", "Chess960"] 32 | 33 | RATED = True 34 | CASUAL = False 35 | RATED_NAMES = ["Casual", "Rated"] 36 | 37 | #LEVELS 38 | LEVEL_EVERYONE = 0 39 | LEVEL_MOD = 1 40 | LEVEL_ADMIN = 2 41 | LEVEL_OWNER = 3 42 | 43 | #FLAGS 44 | FLAG_NONE = 0 45 | FLAG_MUST_BE_IN_GAME = 1 46 | FLAG_MUST_BE_SERVER_OWNER = 2 47 | FLAG_MUST_HAVE_PERM_MANAGE_SERVER = 4 48 | FLAG_MUST_NOT_BE_BLACKLISTED = 8 49 | 50 | # 51 | USER_FLAG_BLACKLISTED = 1 52 | USER_FLAG_TOURNAMENT_1ST = 2 53 | USER_FLAG_TOURNAMENT_2ND = 4 54 | USER_FLAG_PATRON = 8 55 | USER_FLAG_MASTER = 16 56 | 57 | GLICKO_MU = 1200.0 58 | GLICKO_PHI = 250.0 59 | GLICKO_SIGMA = 0.06 60 | GLICKO_TAU = 0.75 61 | 62 | GLICKO_PROVISIONAL_MIN_PHI = 150 63 | 64 | ACCEPT_MARK = "\U00002705" 65 | DENY_MARK = "\U0000274e" 66 | 67 | EMBED_COLOR = 4623620 68 | 69 | MAX_MESSAGE_CACHE = 5000 70 | 71 | DBLURL = "https://top.gg/api/bots/366770566331629579/stats" 72 | 73 | BOTVOTEURL = "https://top.gg/bot/366770566331629579/vote" 74 | SERVERVOTEURL = "https://top.gg/servers/430504476458221570/vote" 75 | 76 | PREFIX = "|" 77 | 78 | CHESSBOTSERVER = 430504476458221570 79 | 80 | ERRORCHANNEL = 433431162107723787 81 | LOGCHANNEL = 436342882551595008 82 | GAMESCHANNEL = 503633867673042953 83 | SUGGESTIONCHANNEL = 441095220038467585 84 | 85 | BADGES = { 86 | "blunder": "\U00002753", 87 | "proficient": "\U00002757", 88 | "brilliant": "\U0000203c", 89 | "tournament-first-place": "\U0001f947", 90 | "tournament-second-place": "\U0001f948", 91 | "developer": "\U00002699", 92 | "admin": "\U0001f440", 93 | "voter": "\U0001f4dd", 94 | "expert": "\U00002694", 95 | "intermediate": "\U0001f5e1", 96 | "novice": "\U0001f4a1", 97 | "addicted": "\U0001f48a", 98 | "master": "\U0001f3c6", 99 | "patron": "\U0001f4b3", 100 | "blacklisted": "\U0001f6ab", 101 | "supporter": "\u2764" 102 | } 103 | 104 | PAGELENGTH = 8 105 | 106 | WINMESSAGES = [ 107 | "{winner} TORE THE HEAD OFF OF--err, won in a chess match against {loser}", 108 | "{winner} just absolutely demolished {loser}", 109 | "{winner} completely outwitted {loser}", 110 | "{winner} has asserted dominance over {loser}", 111 | "{winner} is obviously better at Chess than {loser}" 112 | ] 113 | 114 | DISCORD_LINK = "https://discord.gg/uV5y7RY" 115 | 116 | chess.svg.DEFAULT_COLORS["coord"] = "#f1ad00" 117 | chess.svg.DEFAULT_COLORS["margin"] = "rgba(0,0,0,0)" 118 | 119 | RATING_ROLES = { 120 | 1200: 559561778607161376, 121 | 1300: 559561460787838976, 122 | 1400: 559561680632152064, 123 | 1500: 559561732595384350, 124 | 1600: 559561811440173073, 125 | 1700: 559561870013366273, 126 | 1800: 559561905937711124, 127 | 1900: 559561940586987551, 128 | 2000: 559562032920264705, 129 | } 130 | 131 | SHARDS_PER_PROCESS = 12 132 | PROCESSES = 4 133 | 134 | APM_SERVICE = None 135 | 136 | from chessbot.tok import * # Overwrite defaults 137 | 138 | 139 | DBLHEADERS = {"Authorization" : DBLTOKEN} 140 | -------------------------------------------------------------------------------- /chessbot/db.py: -------------------------------------------------------------------------------- 1 | from chessbot import config 2 | from chessbot.config import * 3 | from chessbot.util import * 4 | 5 | from pymongo import MongoClient 6 | import datetime 7 | import random 8 | 9 | import chess 10 | from bson.objectid import ObjectId 11 | import chess.variant 12 | import chess.pgn 13 | 14 | client = MongoClient() 15 | db = client.chess 16 | 17 | users = db.users 18 | guilds = db.guilds 19 | games = db.games 20 | 21 | 22 | users.create_index("id",unique=True) 23 | users.create_index("rating") 24 | guilds.create_index("id",unique=True) 25 | games.create_index("timestamp") 26 | games.create_index("outcome") 27 | 28 | class DBObject(): 29 | 30 | collection = None 31 | 32 | def __init__(self, d): 33 | if d: 34 | self.exists = True 35 | self._id = d["_id"] 36 | else: 37 | self.exists = False 38 | 39 | @classmethod 40 | def from_id(cls,id): 41 | d = cls.collection.find_one({"_id": ObjectId(id)}) 42 | return cls(d) 43 | 44 | def __bool__(self): 45 | return self.exists 46 | 47 | def __str__(self): 48 | return str(self.__dict__) 49 | 50 | def __repr__(self): 51 | return self.__str__() 52 | 53 | def set(self,key,val): 54 | self.collection.update_one({"_id": ObjectId(self._id)},{"$set": {key:val}}) 55 | 56 | def push(self,key,val): 57 | self.collection.update_one({"_id": ObjectId(self._id)},{"$push": {key:val}}) 58 | 59 | def pull(self,key,val): 60 | self.collection.update_one({"_id": ObjectId(self._id)},{"$pull": {key:val}}) 61 | 62 | def inc(self,key,val): 63 | self.collection.update_one({"_id": ObjectId(self._id)},{"$inc": {key:val}}) 64 | 65 | def pop(self,key,val): 66 | self.collection.update_one({"_id": ObjectId(self._id)},{"$pop": {key:val}}) 67 | 68 | def delete(self): 69 | self.collection.delete_one({"_id": ObjectId(self._id)}) 70 | 71 | class Game(DBObject): 72 | 73 | collection = db.games 74 | 75 | def __init__(self,d): 76 | super().__init__(d) 77 | if not self.exists: return 78 | 79 | self.basefen = d["fen"] 80 | self.variant = d["variant"] 81 | self.board = get_base_board(self) 82 | 83 | self.moves = d["moves"] 84 | for move in self.moves: 85 | self.board.push(chess.Move.from_uci(move)) 86 | 87 | self.fen = self.board.fen() 88 | self.winner = d["winner"] 89 | self.loser = d["loser"] 90 | self.outcome = d["outcome"] 91 | self.done = self.outcome != OUTCOME_UNFINISHED 92 | self.p1 = d["1"] 93 | self.p2 = d["2"] 94 | self.white = d["1"] 95 | self.black = d["2"] 96 | self.players = [self.black,self.white] 97 | self.id = d["_id"] 98 | self.ranked = d["ranked"] 99 | self.valid = d["valid"] 100 | self.timestamp = d["timestamp"] 101 | self.remark = d["remark"] 102 | 103 | @classmethod 104 | def new(cls,u1,u2, variant=VARIANT_STANDARD, fen=None, rated=True): 105 | if fen == None: 106 | if variant == VARIANT_RACINGKINGS:fen = chess.variant.RacingKingsBoard().fen() 107 | elif variant == VARIANT_HORDE:fen = chess.variant.HordeBoard().fen() 108 | elif variant == VARIANT_960: 109 | holder = chess.Board(chess960=True) 110 | boar_num = random.randint(0,960) 111 | holder.set_chess960_pos(boar_num) 112 | fen = holder.fen() 113 | else: fen = chess.Board().fen() 114 | data = {"fen": fen, "moves": [], "winner": None, "loser": None, "outcome": OUTCOME_UNFINISHED, "1": u1, "2": u2, "ranked": rated, "valid": True, "timestamp": datetime.datetime.utcnow(), "variant": variant, "remark": None} 115 | games.insert_one(data) 116 | return Game.from_user_id(u1) 117 | 118 | @classmethod 119 | def from_user_id(cls,userid): 120 | d = db.games.find_one({"$and":[{"outcome": OUTCOME_UNFINISHED}, {"$or": [{"1":userid}, {"2":userid}]}]}) 121 | return cls(d) 122 | 123 | @classmethod 124 | def from_user_id_recent(cls,userid): 125 | try: 126 | d = games.find({"$or": [{"1":userid}, {"2":userid}]}).sort('timestamp',-1).next() 127 | except: 128 | d = None 129 | return cls(d) 130 | 131 | def add_move(self, move): 132 | self.push("moves", move) 133 | 134 | def end(self, winner, loser, outcome): 135 | self.set("winner", winner) 136 | self.set("loser", loser) 137 | self.set("outcome", outcome) 138 | 139 | def pgn(self): 140 | board = get_base_board(self) 141 | pb = chess.pgn.Game().without_tag_roster() 142 | pb.setup(board) 143 | pb.headers["Site"] = BOTURL 144 | pn = pb 145 | for i in self.moves: 146 | pn = pn.add_variation(chess.Move.from_uci(i)) 147 | return pb 148 | 149 | 150 | 151 | class User(DBObject): 152 | 153 | collection = db.users 154 | 155 | def __init__(self,d): 156 | super().__init__(d) 157 | if not self.exists: return 158 | 159 | self.name = d["name"] 160 | self.id = d["id"] 161 | self._id = d["_id"] 162 | 163 | ### STUFF THAT RELIES ON FETCHING GAMES THAT WE WILL LAZY LOAD WHEN NEEDED ### 164 | self._list_of_games = None 165 | self._badges = None 166 | 167 | self.votes = d["votes"] 168 | self.bio = d["bio"] 169 | self.flags = d["flags"] 170 | self.rating = d["rating"] 171 | self.rating_deviation = d["rating_deviation"] 172 | self.rating_volatility = d["rating_volatility"] 173 | self.glicko = glicko_env.create_rating(self.rating, self.rating_deviation, self.rating_volatility) 174 | self.level = d["level"] 175 | 176 | @classmethod 177 | def from_user_id(cls,userid): 178 | d = db.users.find_one({"id": userid}) 179 | return cls(d) 180 | 181 | @classmethod 182 | def new(cls,userid,name): 183 | rating = glicko_env.create_rating() 184 | 185 | data = {"name": name, "id": userid, "flags": 0, "votes": 0, "bio": None, "rating": rating.mu, "rating_deviation": rating.phi, "rating_volatility": rating.sigma, "level": 0} 186 | users.insert_one(data) 187 | 188 | return User.from_user_id(userid) 189 | 190 | @classmethod 191 | def from_mem(cls,mem): 192 | d = User.from_user_id(mem.id) 193 | if not d: 194 | return User.new(mem.id, str(mem)) 195 | else: 196 | return d 197 | 198 | @classmethod 199 | def from_name(cls,name): 200 | d = db.users.find_one({"name": name}) 201 | return cls(d) 202 | 203 | def get_games(self): 204 | gs = games.find({"$or": [{"1":self.id},{"2":self.id}]}).sort('timestamp',-1) 205 | out = [] 206 | for game in gs: 207 | out.append(Game(game)) 208 | return out 209 | 210 | def delete_games(self): 211 | d = games.delete_many({"$or": [{"1":self.id},{"2":self.id}]}) 212 | return d 213 | 214 | def blacklist(self): 215 | db.games.update_many({"$or": [{"1":self.id}, {"2":self.id}]}, {"$set": {"valid": False}}) 216 | self.set('flags', self.flags|USER_FLAG_BLACKLISTED) 217 | rating_sync() 218 | return self 219 | 220 | def unblacklist(self): 221 | db.games.update_many({"$or": [{"1":self.id}, {"2":self.id}]}, {"$set": {"valid": True}}) 222 | self.set('flags', self.flags&~USER_FLAG_BLACKLISTED) 223 | rating_sync() 224 | return self 225 | 226 | def get_rank(self): 227 | rank_cur = db.users.find().sort("rating", -1) 228 | i = 0 229 | 230 | for user in rank_cur: 231 | if user["id"] == self.id: 232 | return i 233 | 234 | i += 1 235 | 236 | def update_glicko(self, glicko): 237 | self.collection.update_one({"_id": ObjectId(self._id)}, {"$set": { 238 | "rating": glicko.mu, 239 | "rating_deviation": glicko.phi, 240 | "rating_volatility": glicko.sigma 241 | }}) 242 | 243 | def render_rating(self): 244 | rendered_rating = int(round(self.rating, 0)) 245 | 246 | if self.rating_deviation >= GLICKO_PROVISIONAL_MIN_PHI: 247 | rendered_rating = f"{rendered_rating}?" 248 | 249 | return rendered_rating 250 | 251 | def list_of_games(self): # Lazy load in the list of games only when needed 252 | if self._list_of_games == None: 253 | self._list_of_games = list(games.find({"$and": [{"$or": [{"1":self.id}, {"2":self.id}]}, {"valid": True}]})) # Get all valid games the user is in 254 | 255 | return self._list_of_games 256 | 257 | def badges(self): 258 | if self._badges == None: 259 | self._badges = [] 260 | if self.level >= LEVEL_OWNER: self._badges.append("developer") 261 | if self.level >= LEVEL_ADMIN: self._badges.append("admin") 262 | if self.win_count() >= 3: self._badges.append("novice") 263 | elif self.win_count() >= 10: self._badges.append("intermediate") 264 | elif self.win_count() >= 20: self._badges.append("expert") 265 | if self.game_count() >= 50: self._badges.append("addicted") 266 | if self.rating >= 1800: self._badges.append("brilliant") 267 | elif self.rating >= 1500: self._badges.append("proficient") 268 | elif self.rating <= 1000: self._badges.append("blunder") 269 | if self.votes >= 5: self._badges.append("voter") 270 | if self.votes >= 50: self._badges.append("supporter") 271 | if self.flags & USER_FLAG_BLACKLISTED: self._badges.append("blacklisted") 272 | if self.flags & USER_FLAG_TOURNAMENT_1ST: self._badges.append("tournament-first-place") 273 | if self.flags & USER_FLAG_TOURNAMENT_2ND: self._badges.append("tournament-second-place") 274 | if self.flags & USER_FLAG_PATRON: self._badges.append("patron") 275 | if self.flags & USER_FLAG_MASTER: self._badges.append("master") 276 | 277 | return self._badges 278 | 279 | def win_count(self): 280 | return len([ 281 | game for game in self.list_of_games() if ( 282 | ( 283 | game["outcome"] == OUTCOME_CHECKMATE or 284 | game["outcome"] == OUTCOME_RESIGN 285 | ) and 286 | game["winner"] == self.id and 287 | game["ranked"] == True 288 | ) 289 | ]) 290 | 291 | def loss_count(self): 292 | return len([ 293 | game for game in self.list_of_games() if ( 294 | ( 295 | game["outcome"] == OUTCOME_CHECKMATE or 296 | game["outcome"] == OUTCOME_RESIGN 297 | ) and 298 | game["loser"] == self.id and 299 | game["ranked"] == True 300 | ) 301 | ]) 302 | 303 | def draw_count(self): 304 | return len([ 305 | game for game in self.list_of_games() if ( 306 | game["outcome"] == OUTCOME_DRAW and 307 | game["ranked"] == True 308 | ) 309 | ]) 310 | 311 | def game_count(self): 312 | return len([ 313 | game for game in self.list_of_games() if ( 314 | ( 315 | game["outcome"] != OUTCOME_EXIT and 316 | game["outcome"] != OUTCOME_UNFINISHED 317 | ) and 318 | game["ranked"] == True 319 | ) 320 | ]) 321 | 322 | 323 | class Guild(DBObject): 324 | 325 | collection = db.guilds 326 | 327 | def __init__(self,d): 328 | super().__init__(d) 329 | if not self.exists: return 330 | 331 | self.name = d["name"] 332 | self.id = d["id"] 333 | self._id = d["_id"] 334 | self.prefix = d["prefix"] 335 | self.calls = d["calls"] 336 | self.games = d["games"] 337 | self.subscribed = d["subscribed"] 338 | 339 | 340 | @classmethod 341 | def from_guild_id(cls,id): 342 | d = db.guilds.find_one({"id": id}) 343 | return cls(d) 344 | 345 | @classmethod 346 | def new(cls,id,name): 347 | data = {"name":name, "id":id, "prefix":PREFIX, "calls": 0, "games": 0, "subscribed": True} 348 | guilds.insert_one(data) 349 | return Guild.from_guild_id(id) 350 | 351 | @classmethod 352 | def from_guild(cls,guild): 353 | d = Guild.from_guild_id(guild.id) 354 | if not d: 355 | return Guild.new(guild.id, guild.name) 356 | else: 357 | return d 358 | 359 | 360 | def leaderboard(limit, sort=-1): 361 | return list(db.users.find().sort("rating", sort).limit(limit)) 362 | 363 | def local_leaderboard(limit, guild): 364 | 365 | guild_member_ids = [member.id for member in guild.members] 366 | 367 | return list(db.users.find({"id": {"$in": guild_member_ids}}).sort("rating",-1).limit(limit)) 368 | 369 | def date_ordered_games(): 370 | return db.games.find().sort("timestamp",1) 371 | -------------------------------------------------------------------------------- /chessbot/glicko2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | glicko2 4 | ~~~~~~~ 5 | The Glicko2 rating system. 6 | :copyright: (c) 2012 by Heungsub Lee 7 | :license: BSD, see LICENSE for more details. 8 | """ 9 | import math 10 | 11 | 12 | __version__ = '0.0.dev' 13 | 14 | 15 | #: The actual score for win 16 | WIN = 1. 17 | #: The actual score for draw 18 | DRAW = 0.5 19 | #: The actual score for loss 20 | LOSS = 0. 21 | 22 | 23 | MU = 1500 24 | PHI = 350 25 | SIGMA = 0.06 26 | TAU = 1.0 27 | EPSILON = 0.000001 28 | #: A constant which is used to standardize the logistic function to 29 | #: `1/(1+exp(-x))` from `1/(1+10^(-r/400))` 30 | Q = math.log(10) / 400 31 | 32 | 33 | class Rating(object): 34 | 35 | def __init__(self, mu=MU, phi=PHI, sigma=SIGMA): 36 | self.mu = mu 37 | self.phi = phi 38 | self.sigma = sigma 39 | 40 | def __repr__(self): 41 | c = type(self) 42 | args = (c.__module__, c.__name__, self.mu, self.phi, self.sigma) 43 | return '%s.%s(mu=%.3f, phi=%.3f, sigma=%.3f)' % args 44 | 45 | 46 | class Glicko2(object): 47 | 48 | def __init__(self, mu=MU, phi=PHI, sigma=SIGMA, tau=TAU, epsilon=EPSILON): 49 | self.mu = mu 50 | self.phi = phi 51 | self.sigma = sigma 52 | self.tau = tau 53 | self.epsilon = epsilon 54 | 55 | def create_rating(self, mu=None, phi=None, sigma=None): 56 | if mu is None: 57 | mu = self.mu 58 | if phi is None: 59 | phi = self.phi 60 | if sigma is None: 61 | sigma = self.sigma 62 | return Rating(mu, phi, sigma) 63 | 64 | def scale_down(self, rating, ratio=173.7178): 65 | mu = (rating.mu - self.mu) / ratio 66 | phi = rating.phi / ratio 67 | return self.create_rating(mu, phi, rating.sigma) 68 | 69 | def scale_up(self, rating, ratio=173.7178): 70 | mu = rating.mu * ratio + self.mu 71 | phi = rating.phi * ratio 72 | return self.create_rating(mu, phi, rating.sigma) 73 | 74 | def reduce_impact(self, rating): 75 | """The original form is `g(RD)`. This function reduces the impact of 76 | games as a function of an opponent's RD. 77 | """ 78 | return 1 / math.sqrt(1 + (3 * rating.phi ** 2) / (math.pi ** 2)) 79 | 80 | def expect_score(self, rating, other_rating, impact): 81 | return 1. / (1 + math.exp(-impact * (rating.mu - other_rating.mu))) 82 | 83 | def determine_sigma(self, rating, difference, variance): 84 | """Determines new sigma.""" 85 | phi = rating.phi 86 | difference_squared = difference ** 2 87 | # 1. Let a = ln(s^2), and define f(x) 88 | alpha = math.log(rating.sigma ** 2) 89 | def f(x): 90 | """This function is twice the conditional log-posterior density of 91 | phi, and is the optimality criterion. 92 | """ 93 | tmp = phi ** 2 + variance + math.exp(x) 94 | a = math.exp(x) * (difference_squared - tmp) / (2 * tmp ** 2) 95 | b = (x - alpha) / (self.tau ** 2) 96 | return a - b 97 | # 2. Set the initial values of the iterative algorithm. 98 | a = alpha 99 | if difference_squared > phi ** 2 + variance: 100 | b = math.log(difference_squared - phi ** 2 - variance) 101 | else: 102 | k = 1 103 | while f(alpha - k * math.sqrt(self.tau ** 2)) < 0: 104 | k += 1 105 | b = alpha - k * math.sqrt(self.tau ** 2) 106 | # 3. Let fA = f(A) and f(B) = f(B) 107 | f_a, f_b = f(a), f(b) 108 | # 4. While |B-A| > e, carry out the following steps. 109 | # (a) Let C = A + (A - B)fA / (fB-fA), and let fC = f(C). 110 | # (b) If fCfB < 0, then set A <- B and fA <- fB; otherwise, just set 111 | # fA <- fA/2. 112 | # (c) Set B <- C and fB <- fC. 113 | # (d) Stop if |B-A| <= e. Repeat the above three steps otherwise. 114 | while abs(b - a) > self.epsilon: 115 | c = a + (a - b) * f_a / (f_b - f_a) 116 | f_c = f(c) 117 | if f_c * f_b < 0: 118 | a, f_a = b, f_b 119 | else: 120 | f_a /= 2 121 | b, f_b = c, f_c 122 | # 5. Once |B-A| <= e, set s' <- e^(A/2) 123 | return math.exp(1) ** (a / 2) 124 | 125 | def rate(self, rating, series): 126 | # Step 2. For each player, convert the rating and RD's onto the 127 | # Glicko-2 scale. 128 | rating = self.scale_down(rating) 129 | # Step 3. Compute the quantity v. This is the estimated variance of the 130 | # team's/player's rating based only on game outcomes. 131 | # Step 4. Compute the quantity difference, the estimated improvement in 132 | # rating by comparing the pre-period rating to the performance 133 | # rating based only on game outcomes. 134 | d_square_inv = 0 135 | variance_inv = 0 136 | difference = 0 137 | if not series: 138 | # If the team didn't play in the series, do only Step 6 139 | phi_star = math.sqrt(rating.phi ** 2 + rating.sigma ** 2) 140 | return self.scale_up(self.create_rating(rating.mu, phi_star, rating.sigma)) 141 | for actual_score, other_rating in series: 142 | other_rating = self.scale_down(other_rating) 143 | impact = self.reduce_impact(other_rating) 144 | expected_score = self.expect_score(rating, other_rating, impact) 145 | variance_inv += impact ** 2 * expected_score * (1 - expected_score) 146 | difference += impact * (actual_score - expected_score) 147 | d_square_inv += ( 148 | expected_score * (1 - expected_score) * 149 | (Q ** 2) * (impact ** 2)) 150 | difference /= variance_inv 151 | variance = 1. / variance_inv 152 | denom = rating.phi ** -2 + d_square_inv 153 | phi = math.sqrt(1 / denom) 154 | # Step 5. Determine the new value, Sigma', ot the sigma. This 155 | # computation requires iteration. 156 | sigma = self.determine_sigma(rating, difference, variance) 157 | # Step 6. Update the rating deviation to the new pre-rating period 158 | # value, Phi*. 159 | phi_star = math.sqrt(phi ** 2 + sigma ** 2) 160 | # Step 7. Update the rating and RD to the new values, Mu' and Phi'. 161 | phi = 1 / math.sqrt(1 / phi_star ** 2 + 1 / variance) 162 | mu = rating.mu + phi ** 2 * (difference / variance) 163 | # Step 8. Convert ratings and RD's back to original scale. 164 | return self.scale_up(self.create_rating(mu, phi, sigma)) 165 | 166 | def rate_1vs1(self, rating1, rating2, drawn=False): 167 | return (self.rate(rating1, [(DRAW if drawn else WIN, rating2)]), 168 | self.rate(rating2, [(DRAW if drawn else LOSS, rating1)])) 169 | 170 | def quality_1vs1(self, rating1, rating2): 171 | expected_score1 = self.expect_score(rating1, rating2, self.reduce_impact(rating1)) 172 | expected_score2 = self.expect_score(rating2, rating1, self.reduce_impact(rating2)) 173 | expected_score = (expected_score1 + expected_score2) / 2 174 | return 2 * (0.5 - abs(0.5 - expected_score)) -------------------------------------------------------------------------------- /chessbot/parameter.py: -------------------------------------------------------------------------------- 1 | import re 2 | from bson.objectid import ObjectId 3 | 4 | class Parameter(): 5 | type_name = "object" 6 | name = "arg" 7 | required = True 8 | 9 | def __init__(self, name = None, required = True): 10 | self.required = required 11 | if name: 12 | self.name = name 13 | 14 | async def parse(self, ctx, arg): 15 | return None 16 | 17 | def usage_string(self): 18 | return "" 19 | 20 | class ParamUser(Parameter): 21 | type_name = "user" 22 | name = "user" 23 | 24 | async def parse(self, ctx, arg): 25 | mention_re = re.search(r"^<@!?(\d+)>$", arg) 26 | id_re = re.search(r"^(\d+)$", arg) 27 | 28 | id = None 29 | 30 | if mention_re: 31 | id = mention_re.group(1) 32 | elif id_re: 33 | id = id_re.group(1) 34 | 35 | try: 36 | id = int(id) 37 | return await ctx.bot.fetch_user(id) 38 | except: 39 | return None 40 | 41 | class ParamGameID(Parameter): 42 | type_name = "game_id" 43 | name = "game" 44 | 45 | async def parse(self, ctx, arg): 46 | try: 47 | ObjectId(arg) 48 | return arg 49 | except: 50 | return None 51 | 52 | class ParamString(Parameter): 53 | type_name = "text" 54 | name = "text" 55 | 56 | async def parse(self, ctx, arg): 57 | return str(arg) 58 | 59 | class ParamInt(Parameter): 60 | type_name = "number" 61 | name = "number" 62 | 63 | async def parse(self, ctx, arg): 64 | try: 65 | return int(arg) 66 | except: 67 | return None 68 | 69 | # This is probably a terrible idea, still think I'm a genius for it though 70 | # You know you're doing something wrong when you self roll type unions 71 | class ParamUnion(Parameter): 72 | name = "query" 73 | def __init__(self, params, name=None, required=True): 74 | super(ParamUnion, self).__init__(name, required) 75 | 76 | self.params = params 77 | self.type_name = "/".join([param.type_name for param in self.params]) 78 | 79 | if not name: 80 | self.name = "/".join([param.name for param in self.params]) 81 | 82 | async def parse(self, ctx, arg): 83 | for param in self.params: 84 | parsed = await param.parse(ctx, arg) 85 | if parsed: 86 | return parsed 87 | 88 | return None 89 | 90 | 91 | class ParamChoice(Parameter): 92 | type_name = "choice" 93 | name = "choice" 94 | 95 | def __init__(self, name=None, required=True, options=None): 96 | super(ParamChoice, self).__init__(name, required) 97 | 98 | self.options = options if options else None 99 | 100 | async def parse(self, ctx, arg): 101 | if str(arg) in self.options: 102 | return str(arg) 103 | 104 | return None 105 | 106 | def usage_string(self): 107 | return "Must be one of: `{}`!".format(", ".join(self.options)) -------------------------------------------------------------------------------- /chessbot/util.py: -------------------------------------------------------------------------------- 1 | from chessbot import config 2 | from chessbot.config import * 3 | from chessbot import db 4 | 5 | import aiohttp 6 | import math 7 | import random 8 | import chess 9 | import discord 10 | import time 11 | import chess.pgn 12 | import chess.svg 13 | import cairosvg 14 | import traceback 15 | from io import BytesIO 16 | 17 | from .glicko2 import Glicko2 18 | 19 | glicko_env = Glicko2(GLICKO_MU, GLICKO_PHI, GLICKO_SIGMA, GLICKO_TAU) 20 | 21 | class Ctx(): 22 | def __init__(self): 23 | self._game = None 24 | 25 | @property 26 | def game(self): 27 | if self._game == None: 28 | print("Game not cached, caching...") 29 | self._game = db.Game.from_user_id(self.mem.id) 30 | else: 31 | print("Using cached version of game") 32 | 33 | return self._game 34 | 35 | async def send_dbl_stats(bot): 36 | if bot.pid == 0: 37 | try: 38 | payload = {"shard_count": SHARDS_PER_PROCESS * PROCESSES, "server_count": len(bot.guilds) * PROCESSES} 39 | async with aiohttp.ClientSession() as aioclient: 40 | await aioclient.post(DBLURL, data=payload, headers=DBLHEADERS) 41 | except: 42 | await log_lone_error(bot, "DBL STATS API", traceback.format_exc()) 43 | 44 | def makeboard(board): 45 | if len(board.move_stack)>0: 46 | bytes = cairosvg.svg2png(bytestring=chess.svg.board(board=board, lastmove=board.peek())) 47 | else: 48 | bytes = cairosvg.svg2png(bytestring=chess.svg.board(board=board)) 49 | bytesio = BytesIO(bytes) 50 | dfile = discord.File(bytesio, filename="board.png") 51 | return dfile 52 | 53 | def ment(id): 54 | return "<@!{}>".format(id) 55 | 56 | def codeblock(s, language=None): 57 | if language != None: 58 | return "```{}\n{}```".format(language,s) 59 | return "```{}```".format(s) 60 | 61 | def rating_sync(): 62 | users = {} 63 | games = db.games.find( 64 | { 65 | "$and": [ 66 | {"ranked": True}, 67 | {"valid": True}, 68 | {"outcome": {"$ne": OUTCOME_EXIT}}, 69 | {"outcome": {"$ne": OUTCOME_UNFINISHED}} 70 | ] 71 | } 72 | ).sort("timestamp",1) 73 | 74 | for game in games: 75 | if not game["1"] in users: 76 | users[game["1"]] = glicko_env.create_rating() 77 | 78 | if not game["2"] in users: 79 | users[game["2"]] = glicko_env.create_rating() 80 | 81 | if game["outcome"] == OUTCOME_DRAW: 82 | new_rating = glicko_env.rate_1vs1(users[game["1"]], users[game["2"]], drawn=True) 83 | users[game["1"]] = new_rating[0] 84 | users[game["2"]] = new_rating[1] 85 | 86 | elif game["outcome"] == OUTCOME_RESIGN or game["outcome"] == OUTCOME_CHECKMATE: 87 | new_rating = glicko_env.rate_1vs1(users[game["winner"]], users[game["loser"]]) 88 | users[game["winner"]] = new_rating[0] 89 | users[game["loser"]] = new_rating[1] 90 | 91 | default_rating = glicko_env.create_rating() 92 | db.users.update_many({}, {"$set": {"rating": default_rating.mu, "rating_deviation": default_rating.phi, "rating_volatility": default_rating.sigma}}) 93 | 94 | for id, rating in users.items(): 95 | db.users.update({"id": id}, {"$set": {"rating": rating.mu, "rating_deviation": rating.phi, "rating_volatility": rating.sigma}}) 96 | 97 | def get_base_board(g): 98 | if g.variant == VARIANT_CRAZYHOUSE: board = chess.variant.CrazyhouseBoard(fen=g.basefen) 99 | elif g.variant == VARIANT_KOTH: board = chess.variant.KingOfTheHillBoard(fen=g.basefen) 100 | elif g.variant == VARIANT_ATOMIC: board = chess.variant.AtomicBoard(fen=g.basefen) 101 | elif g.variant == VARIANT_ANTICHESS: board = chess.variant.AntichessBoard(fen=g.basefen) 102 | elif g.variant == VARIANT_RACINGKINGS: board = chess.variant.RacingKingsBoard(fen=g.basefen) 103 | elif g.variant == VARIANT_HORDE: board = chess.variant.HordeBoard(fen=g.basefen) 104 | elif g.variant == VARIANT_960 or g.variant == VARIANT_STANDARD: board = chess.Board(fen=g.basefen) 105 | 106 | return board 107 | 108 | def pgn_from_game(g): 109 | board = get_base_board(g) 110 | pb = chess.pgn.Game().without_tag_roster() 111 | pb.setup(board) 112 | pb.headers["Site"] = BOTURL 113 | pn = pb 114 | 115 | for i in g.moves: 116 | pn = pn.add_variation(chess.Move.from_uci(i)) 117 | 118 | return pb 119 | 120 | 121 | async def log_command(ctx): 122 | data = "```USER NAME: {}\nUSER ID: {}\nGUILD NAME: {}\nGUILD ID: {}\nCHANNEL NAME: {}\nCHANNEL ID: {}\nMESSAGE ID: {}\nCOMMAND: {}\nARGS: {}```" 123 | data = data.format(ctx.mem, ctx.mem.id, ctx.guild, ctx.guild.id, ctx.ch, ctx.ch.id, ctx.msg.id, ctx.command, ctx.args) 124 | await ctx.bot.log_channel.send(data) 125 | 126 | async def log_error(bot, msg, error): 127 | data = "```USER NAME: {}\nUSER ID: {}\nGUILD NAME: {}\nGUILD ID: {}\nCHANNEL NAME: {}\nCHANNEL ID: {}\nMESSAGE: {}\nMESSAGE ID: {}\nTRACEBACK:\n\n{}```" 128 | data = data.format(msg.author, msg.author.id, msg.guild, msg.guild.id, msg.channel, msg.channel.id, msg.content, msg.id,error) 129 | await bot.error_channel.send(data) 130 | 131 | async def log_lone_error(bot, event, error): 132 | data = "```ERROR IN: {}\nTRACEBACK:\n\n{}```" 133 | data = data.format(event, error) 134 | await bot.error_channel.send(data) 135 | 136 | 137 | async def reward_game(winner,loser,outcome, game, channel, bot): 138 | winner = db.User.from_user_id(winner) 139 | loser = db.User.from_user_id(loser) 140 | guild = channel.guild 141 | if game.ranked: 142 | if (outcome == OUTCOME_RESIGN and len(game.moves) > 2) or outcome == OUTCOME_CHECKMATE: 143 | new_rating = glicko_env.rate_1vs1(winner.glicko, loser.glicko) 144 | winner.update_glicko(new_rating[0]) 145 | loser.update_glicko(new_rating[1]) 146 | 147 | if outcome == OUTCOME_CHECKMATE: 148 | if game.ranked: 149 | await channel.send(random.choice(WINMESSAGES).format(winner=ment(winner.id), loser=ment(loser.id))+"! Checkmate! ({}/{})".format(int(round(new_rating[0].mu-winner.rating, 0)),int(round(new_rating[1].mu-loser.rating, 0)))) 150 | else: 151 | await channel.send(random.choice(WINMESSAGES).format(winner=ment(winner.id), loser=ment(loser.id))+"! Checkmate!") 152 | 153 | game.end(winner.id, loser.id, OUTCOME_CHECKMATE) 154 | 155 | if outcome == OUTCOME_RESIGN: 156 | if len(game.moves) > 2: 157 | if game.ranked: 158 | await channel.send("You have resigned! <@!"+str(winner.id)+"> wins! ({}/{})".format(int(round(new_rating[0].mu-winner.rating, 0)),int(round(new_rating[1].mu-loser.rating, 0)))) 159 | else: 160 | await channel.send("You have resigned! <@!"+str(winner.id)+"> wins!") 161 | game.end(winner.id, loser.id, OUTCOME_RESIGN) 162 | else: 163 | await channel.send("You have resigned! <@!"+str(winner.id)+"> wins!") 164 | game.delete() 165 | 166 | if outcome == OUTCOME_DRAW: 167 | if len(game.moves) > 2: 168 | if game.ranked: 169 | new_rating = glicko_env.rate_1vs1(winner.glicko, loser.glicko, drawn=True) 170 | winner.update_glicko(new_rating[0]) 171 | loser.update_glicko(new_rating[1]) 172 | 173 | await channel.send("The game is a draw! Game over! ({}/{})".format(int(round(new_rating[0].mu-winner.rating, 0)),int(round(new_rating[1].mu-loser.rating, 0)))) 174 | 175 | else: 176 | await channel.send("The game is a draw! Game over!") 177 | game.end(None, None, OUTCOME_DRAW) 178 | else: 179 | await channel.send("The game is a draw! Game over!") 180 | game.delete() 181 | 182 | if outcome == OUTCOME_EXIT: 183 | await channel.send('You have exited the game!') 184 | if len(game.moves) > 2: 185 | game.end(None, None, OUTCOME_EXIT) 186 | else: 187 | game.delete() 188 | 189 | g = db.Game.from_id(game.id) 190 | if g: 191 | ch = await bot.fetch_channel(GAMESCHANNEL) 192 | await ch.send(embed=embed_from_game(g)) 193 | 194 | await update_activity(bot) 195 | 196 | def embed_from_game(game): 197 | em = discord.Embed() 198 | em.title="Game "+str(game.id) 199 | em.colour = discord.Colour(EMBED_COLOR) 200 | em.type = "rich" 201 | if game.remark: 202 | em.description = "> {}\n```{}```".format(game.remark, str(pgn_from_game(game))) 203 | else: 204 | em.description = "```{}```".format(str(pgn_from_game(game))) 205 | em.add_field(name="White",value=db.User.from_user_id(game.white).name,inline=True) 206 | em.add_field(name="Black",value=db.User.from_user_id(game.black).name,inline=True) 207 | em.add_field(name="Outcome",value=OUTCOME_NAMES[game.outcome],inline=True) 208 | 209 | if game.outcome in [OUTCOME_CHECKMATE, OUTCOME_RESIGN]: 210 | em.add_field(name="Winner",value=db.User.from_user_id(game.winner).name,inline=True) 211 | else: 212 | em.add_field(name="Winner",value=str(None),inline=True) 213 | 214 | em.add_field(name="Timestamp",value=str(game.timestamp.strftime('%m-%d-%Y %H:%M:%S')),inline=True) 215 | em.add_field(name="Ranked",value=str(game.ranked),inline=True) 216 | 217 | if game.variant != VARIANT_STANDARD: 218 | em.add_field(name="Variant",value=VARIANT_NAMES[game.variant],inline=True) 219 | 220 | return em 221 | 222 | async def update_rating_roles(ctx): 223 | guild = ctx.bot.get_guild(CHESSBOTSERVER) 224 | rmroles = [guild.get_role(r) for r in RATING_ROLES.values()] 225 | for member in guild.members: 226 | user = db.User.from_mem(member) 227 | rating = int((user.rating)/100)*100 228 | rs = [r for r in rmroles if r in member.roles] 229 | if len(rs) > 0: 230 | await member.remove_roles(*rs) 231 | if user.game_count() > 0: 232 | if rating in RATING_ROLES: 233 | await member.add_roles(guild.get_role(RATING_ROLES[rating])) 234 | 235 | 236 | async def update_activity(bot): 237 | await bot.change_presence(activity=discord.Game(name="{} / {} games!".format( 238 | db.games.find({"outcome": OUTCOME_UNFINISHED}).count(), 239 | db.games.count_documents({}) 240 | )), status=discord.Status.online) -------------------------------------------------------------------------------- /chessbot/website/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, abort, request, send_from_directory 2 | 3 | from chessbot.config import * 4 | from chessbot import config 5 | from chessbot import db 6 | 7 | app = Flask(__name__) 8 | 9 | app.url_map.strict_slashes = False 10 | 11 | 12 | @app.context_processor 13 | def context_processor(): 14 | return dict(cfg=config, db=db) 15 | 16 | 17 | @app.route('/static/') 18 | def static_content(path): 19 | return send_from_directory('./static', path) 20 | 21 | 22 | from chessbot.website.modules.api import blueprint_api 23 | from chessbot.website.modules.home import blueprint_home 24 | 25 | app.register_blueprint(blueprint_api) 26 | app.register_blueprint(blueprint_home) -------------------------------------------------------------------------------- /chessbot/website/modules/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qwertyquerty/ChessBot/1f126fc9b2ddaec49603a1765bdc689b98e4a16f/chessbot/website/modules/__init__.py -------------------------------------------------------------------------------- /chessbot/website/modules/api.py: -------------------------------------------------------------------------------- 1 | from flask import abort, Blueprint, request, Response 2 | 3 | from chessbot.config import * 4 | from chessbot import db 5 | 6 | blueprint_api = Blueprint('api', __name__) 7 | 8 | @blueprint_api.route("/api/vote", methods = ['POST']) 9 | def page_api_vote(): 10 | 11 | if request.headers["Authorization"] != WEBHOOK_TOKEN: 12 | return abort(401) 13 | 14 | uid = int(request.json["user"]) 15 | 16 | user = db.User.from_user_id(uid) 17 | 18 | if not user: 19 | return abort(400) 20 | 21 | user.inc("votes", 1) 22 | return Response(status=200) 23 | -------------------------------------------------------------------------------- /chessbot/website/modules/home.py: -------------------------------------------------------------------------------- 1 | from flask import abort, Blueprint, redirect, request, render_template, Response 2 | 3 | from chessbot.config import * 4 | from chessbot import db 5 | from chessbot.command import Command 6 | from chessbot.commands import * 7 | 8 | import chess 9 | import chess.svg 10 | 11 | blueprint_home = Blueprint('home', __name__) 12 | 13 | @blueprint_home.route("/") 14 | def page_index(): 15 | return render_template("home.html") 16 | 17 | 18 | @blueprint_home.route("/game_image/") 19 | def page_game_image(game_id): 20 | try: 21 | game = db.Game.from_id(game_id) 22 | except: 23 | return abort(400) 24 | 25 | if not game: 26 | return abort(404) 27 | 28 | if len(game.board.move_stack) > 0: 29 | board_svg = chess.svg.board(game.board, lastmove=game.board.peek()) 30 | else: 31 | board_svg = chess.svg.board(game.board) 32 | 33 | return Response(board_svg, content_type="image/svg+xml") 34 | 35 | 36 | @blueprint_home.route("/leaderboard") 37 | def page_leaderboard(): 38 | return render_template("leaderboard.html") 39 | 40 | 41 | @blueprint_home.route("/commands") 42 | def page_commands(): 43 | available_commands = [command for command in Command.__subclasses__() if command.level == LEVEL_EVERYONE] 44 | sorted_commands = sorted(available_commands, key = lambda x: x.help_index) 45 | 46 | return render_template("commands.html", commands=sorted_commands, prefix=PREFIX) 47 | 48 | @blueprint_home.route("/big_stats") 49 | def page_big_stats(): 50 | big_stats = [ 51 | {"name": "Active Games", "value": db.games.count_documents({"outcome": OUTCOME_UNFINISHED})}, 52 | {"name": "Total Games", "value": db.games.count_documents({})}, 53 | {"name": "Total Users", "value": db.users.count_documents({})} 54 | ] 55 | 56 | return render_template("big_stats.html", big_stats=big_stats) 57 | 58 | 59 | @blueprint_home.route("/user/") 60 | def page_user(id): 61 | 62 | user = db.User.from_user_id(id) 63 | 64 | if not user: 65 | abort(404) 66 | 67 | return render_template("user.html", user=user) 68 | 69 | 70 | @blueprint_home.route("/invite") 71 | def page_invite(): 72 | return redirect(BOT_INVITE_LINK) 73 | 74 | 75 | @blueprint_home.route("/github") 76 | def page_github(): 77 | return redirect(GITHUB_LINK) -------------------------------------------------------------------------------- /chessbot/website/static/css/big_stats.css: -------------------------------------------------------------------------------- 1 | .big-stats { 2 | display: flex; 3 | justify-content: space-between; 4 | } 5 | 6 | .big-stat { 7 | display: flex-item; 8 | border-bottom: solid var(--chess-border-thick) var(--chess-green); 9 | padding: var(--chess-padding-medium); 10 | font-size: var(--chess-text-huge); 11 | text-align: center; 12 | flex-grow: 1; 13 | background-color: var(--chess-dark-gray-2); 14 | margin: 0px var(--chess-padding-thick) 15 | } 16 | 17 | .big-stat label { 18 | font-size: var(--chess-text-normal); 19 | text-decoration: underline; 20 | color: var(--chess-gray); 21 | } -------------------------------------------------------------------------------- /chessbot/website/static/css/commands.css: -------------------------------------------------------------------------------- 1 | .command-entry { 2 | background-color: var(--chess-dark-gray-2); 3 | font-size: var(--chess-text-normal); 4 | padding: var(--chess-padding-medium); 5 | border-bottom: solid var(--chess-border-thick) var(--chess-green); 6 | margin-bottom: var(--chess-padding-normal); 7 | } -------------------------------------------------------------------------------- /chessbot/website/static/css/home.css: -------------------------------------------------------------------------------- 1 | .active-game { 2 | text-align: center; 3 | } 4 | 5 | #active-game-image { 6 | max-height: 80vh; 7 | max-width: 90vw; 8 | } -------------------------------------------------------------------------------- /chessbot/website/static/css/leaderboard.css: -------------------------------------------------------------------------------- 1 | .leaderboard-entry { 2 | background-color: var(--chess-dark-gray-2); 3 | font-size: var(--chess-text-normal); 4 | padding: var(--chess-padding-medium); 5 | border-bottom: solid var(--chess-border-thick) var(--chess-green); 6 | margin-bottom: var(--chess-padding-normal); 7 | } -------------------------------------------------------------------------------- /chessbot/website/static/css/main.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --chess-green: #468d04; 3 | --chess-white: #ffffff; 4 | --chess-black: #000000; 5 | --chess-gray: #aaaaaa; 6 | --chess-dark-gray-1: #353535; 7 | --chess-dark-gray-2: #242424; 8 | --chess-dark-gray-3: #1c1c1c; 9 | --chess-dark-gray-4: #121110; 10 | 11 | --chess-border-thick: 4px; 12 | --chess-border-thin: 1px; 13 | 14 | --chess-text-tiny: 12px; 15 | --chess-text-body: 16px; 16 | --chess-text-normal: 20px; 17 | --chess-text-medium: 25px; 18 | --chess-text-big: 30px; 19 | --chess-text-huge: 40px; 20 | --chess-text-mega: 60px; 21 | 22 | --chess-padding-thick: 30px; 23 | --chess-padding-big: 20px; 24 | --chess-padding-medium: 15px; 25 | --chess-padding-normal: 8px; 26 | --chess-padding-thin: 5px; 27 | --chess-padding-very-thin: 2px; 28 | --chess-padding-none: 0px; 29 | } 30 | 31 | .text-tiny {font-size: var(--chess-text-tiny);} 32 | .text-body {font-size: var(--chess-text-body);} 33 | .text-normal {font-size: var(--chess-text-normal);} 34 | .text-big {font-size: var(--chess-text-big);} 35 | .text-huge {font-size: var(--chess-text-huge);} 36 | .text-mega {font-size: var(--chess-text-mega);} 37 | 38 | 39 | html, body { 40 | background-color: var(--chess-dark-gray-1); 41 | margin: 0px; 42 | color: var(--chess-white); 43 | font-family: Arial; 44 | box-sizing: border-box; 45 | overflow-x: auto; 46 | } 47 | 48 | a { 49 | color: var(--chess-white); 50 | } 51 | 52 | 53 | #nav { 54 | display: inline-block; 55 | height: 45px; 56 | width: 100%; 57 | display: flex; 58 | background-color: var(--chess-dark-gray-3); 59 | font-size: var(--chess-text-normal); 60 | justify-content: space-between; 61 | letter-spacing: 1px; 62 | border-bottom: solid var(--chess-border-thick) var(--chess-green); 63 | } 64 | 65 | .nav-item { 66 | color: var(--chess-white); 67 | transition: text-decoration 0.2s; 68 | cursor: pointer; 69 | flex-grow: 1; 70 | text-align: center; 71 | padding: var(--chess-padding-normal) var(--chess-padding-thin); 72 | min-width: 150px; 73 | max-width: 150px; 74 | user-select: none; 75 | font-size: var(--chess-text-normal); 76 | text-decoration: none; 77 | line-height: 30px; 78 | } 79 | 80 | .nav-item:hover, .nav-item:focus { 81 | text-decoration: underline; 82 | outline: none; 83 | } 84 | 85 | #nav-spacer { 86 | flex-grow: 1; 87 | } 88 | 89 | 90 | #content { 91 | padding: var(--chess-padding-medium); 92 | } 93 | 94 | 95 | .right { 96 | float: right; 97 | } -------------------------------------------------------------------------------- /chessbot/website/static/css/user.css: -------------------------------------------------------------------------------- 1 | .user-profile { 2 | background-color: var(--chess-dark-gray-2); 3 | font-size: var(--chess-text-normal); 4 | padding: var(--chess-padding-medium); 5 | border-bottom: solid var(--chess-border-thick) var(--chess-green); 6 | margin-bottom: var(--chess-padding-normal); 7 | } 8 | 9 | .user-profile-name { 10 | font-size: var(--chess-text-huge); 11 | text-align: center; 12 | } 13 | 14 | .user-profile-bio { 15 | font-size: var(--chess-text-medium); 16 | color: var(--chess-gray); 17 | text-align: center; 18 | font-style: italic; 19 | } 20 | 21 | .user-profile-stats { 22 | text-align: center; 23 | } 24 | 25 | .user-profile-stat { 26 | display: inline-block; 27 | text-align: center; 28 | width: 200px; 29 | font-size: var(--chess-text-medium); 30 | } 31 | 32 | .user-profile-stat-caption { 33 | color: var(--chess-green); 34 | font-weight: bold; 35 | } -------------------------------------------------------------------------------- /chessbot/website/static/js/home.js: -------------------------------------------------------------------------------- 1 | window.onload = function() { 2 | var image = document.getElementById("active-game-image"); 3 | 4 | function update_active_game() { 5 | image.src = image.src.split("?")[0] + "?" + new Date().getTime(); 6 | } 7 | 8 | setInterval(update_active_game, 10000); 9 | } -------------------------------------------------------------------------------- /chessbot/website/templates/big_stats.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}Commands{% endblock %} 4 | {% block active %}commands{% endblock %} 5 | 6 | {% block head %} 7 | 8 | 9 | 10 | {% endblock %} 11 | 12 | {% block content %} 13 |
14 | {% for stat in big_stats %} 15 | 16 |
17 | 18 |
19 | {{ stat["value"] }} 20 |
21 | 22 | {% endfor %} 23 |
24 | 25 | {% endblock %} -------------------------------------------------------------------------------- /chessbot/website/templates/commands.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}Commands{% endblock %} 4 | {% block active %}commands{% endblock %} 5 | 6 | {% block head %} 7 | 8 | 9 | 10 | {% endblock %} 11 | 12 | {% block content %} 13 | 14 |

15 | <> = Required 16 | [] = Optional 17 |

18 | 19 | {% for command in commands %} 20 | 21 |


{{ command.help_string }}
22 | 23 | {% endfor %} 24 | 25 | {% endblock %} -------------------------------------------------------------------------------- /chessbot/website/templates/home.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}Home{% endblock %} 3 | {% block active %}home{% endblock %} 4 | 5 | {% block head %} 6 | 7 | 8 | 9 | {% endblock %} 10 | 11 | {% block content %} 12 |
13 | {% set active_game = db.Game(db.games.find().sort("timestamp", -1)[0]) %} 14 |
Game {{ active_game.id }}
15 |
16 | 17 |
18 | {% endblock %} -------------------------------------------------------------------------------- /chessbot/website/templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | {% block head %} 11 | 12 | {% endblock %} 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ChessBot / {% block title %}{% endblock %} 22 | 23 | 24 | 25 | 26 | 27 |
28 | 35 | 36 |
37 | {% block content %}{% endblock %} 38 |
39 |
40 | 41 | -------------------------------------------------------------------------------- /chessbot/website/templates/leaderboard.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}Leaderboard{% endblock %} 3 | {% block active %}leaderboard{% endblock %} 4 | 5 | {% block head %} 6 | 7 | {% endblock %} 8 | 9 | {% block content %} 10 | 11 | {% for user in db.leaderboard(100) %} 12 |
{{ loop.index }}. {{ user["name"] }} {{ user["rating"] | round | int }}
13 | {% endfor %} 14 | 15 | {% endblock %} -------------------------------------------------------------------------------- /chessbot/website/templates/user.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}User / {{ user.name }}{% endblock %} 3 | {% block active %}leaderboard{% endblock %} 4 | 5 | {% block head %} 6 | 7 | 8 | 9 | 10 | 11 | {% endblock %} 12 | 13 | {% block content %} 14 | 66 | {% endblock %} -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | CairoSVG==2.4.2 2 | discord.py==1.3.4 3 | psutil==5.7.0 4 | pymongo==3.10.1 5 | python-chess==0.31.2 6 | Flask==1.1.1 7 | elastic-apm==5.9.0 -------------------------------------------------------------------------------- /run_chessbot.py: -------------------------------------------------------------------------------- 1 | from chessbot.config import * 2 | from chessbot.bot import ChessBot 3 | 4 | from multiprocessing import Process 5 | 6 | from time import sleep 7 | 8 | def begin_process(pid): 9 | bot = ChessBot( 10 | pid = pid, 11 | max_messages = MAX_MESSAGE_CACHE, 12 | shard_ids = list(range(SHARDS_PER_PROCESS * pid, SHARDS_PER_PROCESS * (pid+1))), 13 | shard_count = PROCESSES * SHARDS_PER_PROCESS 14 | ) 15 | 16 | bot.run(BOTTOKEN) 17 | 18 | processes = {} 19 | 20 | if __name__ == '__main__': 21 | for pid in range(PROCESSES): 22 | p = Process(target=begin_process, args=(pid,)) 23 | processes[pid] = p 24 | p.start() 25 | 26 | 27 | while 1: 28 | sleep(1) 29 | for pid in list(processes.keys()): 30 | if not processes[pid].is_alive(): 31 | print("Process {} failed! Restarting...".format(pid)) 32 | 33 | del processes[pid] 34 | 35 | p = Process(target=begin_process, args=(pid,)) 36 | processes[pid] = p 37 | p.start() 38 | 39 | -------------------------------------------------------------------------------- /run_website.py: -------------------------------------------------------------------------------- 1 | from chessbot.config import * 2 | from chessbot.website import app 3 | 4 | app.run("localhost", port=WEBHOOK_PORT, debug=True) --------------------------------------------------------------------------------