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