├── .gitignore ├── 10k.txt ├── CNAME ├── LICENSE ├── README.md ├── codenames.py ├── compress.py ├── convert.py ├── get_glove.sh ├── index.html ├── static └── codewords_og.png ├── stopwords ├── website ├── README.md ├── favicon.png ├── game.js ├── model │ ├── angel.gz │ ├── angel2.gz │ ├── angel3.gz │ ├── stopwords │ ├── vecs.gz │ ├── vecs.gz.old │ ├── wordlist │ └── words.gz └── styles.css └── wordlist /.gitignore: -------------------------------------------------------------------------------- 1 | # Codenames stuff 2 | log_file 3 | dataset/ 4 | 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | *.swp 11 | TODO 12 | 13 | extract2.py 14 | training.data* 15 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | codewords.app -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Play Codenames with Glove 2 | ========================= 3 | 4 | This repository implements a simple single-player version of the codenames game 5 | by Vlaada Chvátil. 6 | You can play as the agent or the spymaster, and the Glove word vectors will 7 | take the role of your partner, as you try to find the 8 marked words in as few 8 | rounds as possible. 9 | 10 |
11 | $ git clone git@github.com:thomasahle/codenames.git
12 | ...
13 | 
14 | $ sh get_glove.sh
15 | ...
16 | 
17 | $ python3 codenames.py
18 | ...Loading vectors
19 | ...Loading words
20 | ...Making word to index dict
21 | ...Loading codenames
22 | Ready!
23 | 
24 | Will you be agent or spymaster?: agent
25 | 
26 |      buck       bat   pumpkin    charge      iron
27 |      well      boot     chick superhero     glove
28 |    stream   germany      sock    dragon scientist
29 |      duck     bugle    school       ham   mammoth
30 |    bridge      fair  triangle   capital      horn
31 | 
32 | Thinking....................
33 | 
34 | Clue: "golden 6" (certainty 7.78, remaining words 8)
35 | 
36 | Your guess: bridge
37 | Correct!
38 | 
39 | 40 | How it works 41 | ============ 42 | The bot decides what words go well together, by comparing their vectors in the GloVe trained on Wikipedia text. 43 | This means that words that often occour in the same articles and sentences are judged to be similar. 44 | In the example about, golden is of course similar to bridge by association with the Golden Gate Bridge. 45 | Other words that were found to be similar were 'dragon', 'triangle', 'duck', 'iron' and 'horn'. 46 | 47 | However, in Codenames the task is not merely to find words that describe other words well. 48 | You also need to make sure that 'bad words' are as different as possible from your clue. 49 | To achieve this, the bot tries to find a word that maximizes the similarity gap between the marked words and the bad words. 50 | 51 | If you want the bot to be more aggressive in its clues (choosing larger groups), try changing the `agg = .5` value near the top of `codenames.py` to a larger value, such as `.8` or `1.5`. 52 | -------------------------------------------------------------------------------- /codenames.py: -------------------------------------------------------------------------------- 1 | import random 2 | import re 3 | import numpy as np 4 | import math 5 | 6 | from typing import List, Tuple, Iterable 7 | 8 | # This file stores the "solutions" the bot had intended, 9 | # when you play as agent and the bot as spymaster. 10 | log_file = open("log_file", "w") 11 | 12 | 13 | class Reader: 14 | def read_picks( 15 | self, words: List[str], my_words: Iterable[str], cnt: int 16 | ) -> List[str]: 17 | """ 18 | Query the user for guesses. 19 | :param words: Words the user can choose from. 20 | :param my_words: Correct words. 21 | :param cnt: Number of guesses the user has. 22 | :return: The words picked by the user. 23 | """ 24 | raise NotImplementedError 25 | 26 | def read_clue(self, word_set: Iterable[str]) -> Tuple[str, int]: 27 | """ 28 | Read a clue from the (spymaster) user. 29 | :param word_set: Valid words 30 | :return: The clue and number given. 31 | """ 32 | raise NotImplementedError 33 | 34 | def print_words(self, words: List[str], nrows: int): 35 | """ 36 | Prints a list of words as a 2d table, using `nrows` rows. 37 | :param words: Words to be printed. 38 | :param nrows: Number of rows to print. 39 | """ 40 | raise NotImplementedError 41 | 42 | 43 | class TerminalReader(Reader): 44 | def read_picks( 45 | self, words: List[str], my_words: Iterable[str], cnt: int 46 | ) -> List[str]: 47 | picks = [] 48 | while len(picks) < cnt: 49 | guess = None 50 | while guess not in words: 51 | guess = input("Your guess: ").strip().lower() 52 | picks.append(guess) 53 | if guess in my_words: 54 | print("Correct!") 55 | else: 56 | print("Wrong :(") 57 | break 58 | return picks 59 | 60 | def read_clue(self, word_set) -> Tuple[str, int]: 61 | while True: 62 | inp = input("Clue (e.g. 'car 2'): ").lower() 63 | match = re.match("(\w+)\s+(\d+)", inp) 64 | if match: 65 | clue, cnt = match.groups() 66 | if clue not in word_set: 67 | print("I don't understand that word.") 68 | continue 69 | return clue, int(cnt) 70 | 71 | def print_words(self, words: List[str], nrows: int): 72 | longest = max(map(len, words)) 73 | print() 74 | for row in zip(*(iter(words),) * nrows): 75 | for word in row: 76 | print(word.rjust(longest), end=" ") 77 | print() 78 | print() 79 | 80 | 81 | class Codenames: 82 | def __init__(self, cnt_rows=5, cnt_cols=5, cnt_agents=8, agg=.6, shift=.99): 83 | """ 84 | :param cnt_rows: Number of rows to show. 85 | :param cnt_cols: Number of columns to show. 86 | :param cnt_agents: Number of good words. 87 | :param agg: Agressiveness in [0, infinity). Higher means more aggressive. 88 | """ 89 | self.cnt_rows = cnt_rows 90 | self.cnt_cols = cnt_cols 91 | self.cnt_agents = cnt_agents 92 | self.agg = agg 93 | self.shift = shift 94 | 95 | # Other 96 | self.vectors = np.array([]) 97 | self.word_list = [] 98 | self.weirdness = [] 99 | self.word_to_index = {} 100 | self.codenames = [] 101 | 102 | def load(self, datadir): 103 | # Glove word vectors 104 | print("...Loading vectors") 105 | self.vectors = np.load(f"{datadir}/glove.6B.300d.npy") 106 | 107 | # List of all glove words 108 | print("...Loading words") 109 | self.word_list = [w.lower().strip() for w in open(f"{datadir}/words")] 110 | self.weirdness = [math.log(i + 1) + 1 for i in range(len(self.word_list))] 111 | 112 | # Indexing back from word to indices 113 | print("...Making word to index dict") 114 | self.word_to_index = {w: i for i, w in enumerate(self.word_list)} 115 | 116 | # Get rid of stupid hints like "the" 117 | self.stopwords = [w.strip() for w in open('stopwords')] 118 | for w in self.stopwords: 119 | self.weirdness[self.word_to_index[w]] += 5 120 | 121 | # All words that are allowed to go onto the table 122 | print("...Loading codenames") 123 | self.codenames: List[str] = [ 124 | word 125 | for word in (w.lower().strip().replace(" ", "-") for w in open("wordlist2")) 126 | if word in self.word_to_index 127 | ] 128 | 129 | print("Ready!") 130 | 131 | def word_to_vector(self, word: str) -> np.ndarray: 132 | """ 133 | :param word: To be vectorized. 134 | :return: The vector. 135 | """ 136 | return self.vectors[self.word_to_index[word]] 137 | 138 | def most_similar_to_given(self, clue: str, choices: List[str]) -> str: 139 | """ 140 | :param clue: Clue from the spymaster. 141 | :param choices: Choices on the table. 142 | :return: Which choice to go for. 143 | """ 144 | clue_vector = self.word_to_vector(clue) 145 | return max(choices, key=lambda w: self.word_to_vector(w) @ clue_vector) 146 | 147 | def find_clue( 148 | self, words: List[str], my_words: List[str], black_list: Iterable[str], 149 | verbose: bool=False, 150 | ) -> Tuple[str, float, List[str]]: 151 | """ 152 | :param words: Words on the board. 153 | :param my_words: Words we want to guess. 154 | :param black_list: Clues we are not allowed to give. 155 | :return: (The best clue, the score, the words we expect to be guessed) 156 | """ 157 | if verbose: 158 | print("Thinking", end="", flush=True) 159 | 160 | # Words to avoid the agent guessing. 161 | negs = [w for w in words if w not in my_words] 162 | # Worst (highest) inner product with negative words 163 | if negs: 164 | nm = ( 165 | self.vectors @ np.array([self.word_to_vector(word) for word in negs]).T 166 | ).max(axis=1) 167 | else: 168 | # The case where we only have my_words left 169 | nm = [-1000] * len(self.word_list) 170 | # Inner product with positive words 171 | pm = self.vectors @ np.array([self.word_to_vector(word) for word in my_words]).T 172 | 173 | best_clue, best_score, best_k, best_g = None, -100, 0, () 174 | for step, (clue, lower_bound, scores) in enumerate(zip(self.word_list, nm, pm)): 175 | if verbose and step % 20000 == 0: 176 | print(".", end="", flush=True) 177 | 178 | # If the best score is lower than the lower bound, there is no reason 179 | # to even try it. 180 | if max(scores) <= lower_bound or clue in black_list: 181 | continue 182 | 183 | # Order scores by lowest to highest inner product with the clue. 184 | ss = sorted((s, i) for i, s in enumerate(scores)) 185 | # Calculate the "real score" by 186 | # (lowest score in group) * [ (group size)^aggressiveness - 1]. 187 | # The reason we subtract one is that we never want to have a group of 188 | # size 1. 189 | # We divide by log(step), as to not show too many 'weird' words. 190 | real_score, j = max( 191 | ( 192 | (s - lower_bound) 193 | * ((len(ss) - j) ** self.agg - self.shift) 194 | / self.weirdness[step], 195 | j, 196 | ) 197 | for j, (s, _) in enumerate(ss) 198 | ) 199 | 200 | if real_score > best_score: 201 | group = [my_words[i] for _, i in ss[j:]] 202 | best_clue, best_score, best_k, best_g = ( 203 | clue, 204 | real_score, 205 | len(group), 206 | group, 207 | ) 208 | 209 | # After printing '.'s with end="" we need a clean line. 210 | if verbose: 211 | print() 212 | 213 | return best_clue, best_score, best_g 214 | 215 | def play_spymaster(self, reader: Reader): 216 | """ 217 | Play a complete game, with the robot being the spymaster. 218 | """ 219 | words = random.sample(self.codenames, self.cnt_rows * self.cnt_cols) 220 | my_words = set(random.sample(words, self.cnt_agents)) 221 | used_clues = set(my_words) 222 | while my_words: 223 | reader.print_words(words, nrows=self.cnt_rows) 224 | 225 | clue, score, group = self.find_clue(words, list(my_words), used_clues) 226 | # Print the clue to the log_file for "debugging" purposes 227 | group_scores = np.array( 228 | [self.word_to_vector(w) for w in group] 229 | ) @ self.word_to_vector(clue) 230 | print(clue, group, group_scores, file=log_file, flush=True) 231 | # Save the clue, so we don't use it again 232 | used_clues.add(clue) 233 | 234 | print() 235 | print( 236 | 'Clue: "{} {}" (certainty {:.2f}, remaining words {})'.format( 237 | clue, len(group), score, len(my_words) 238 | ) 239 | ) 240 | print() 241 | for pick in reader.read_picks(words, my_words, len(group)): 242 | words[words.index(pick)] = "---" 243 | if pick in my_words: 244 | my_words.remove(pick) 245 | 246 | def play_agent(self, reader: Reader): 247 | """ 248 | Play a complete game, with the robot being the agent. 249 | """ 250 | words = random.sample(self.codenames, self.cnt_rows * self.cnt_cols) 251 | my_words = random.sample(words, self.cnt_agents) 252 | picked = [] 253 | while any(w not in picked for w in my_words): 254 | reader.print_words( 255 | [w if w not in picked else "---" for w in words], nrows=self.cnt_rows 256 | ) 257 | print("Your words:", ", ".join(w for w in my_words if w not in picked)) 258 | clue, cnt = reader.read_clue(self.word_to_index.keys()) 259 | for _ in range(cnt): 260 | guess = self.most_similar_to_given( 261 | clue, [w for w in words if w not in picked] 262 | ) 263 | picked.append(guess) 264 | answer = input("I guess {}? [Y/n]: ".format(guess)) 265 | if answer == "n": 266 | print("Sorry about that.") 267 | break 268 | else: 269 | print("I got them all!") 270 | 271 | 272 | def main(): 273 | cn = Codenames() 274 | cn.load("dataset") 275 | reader = TerminalReader() 276 | while True: 277 | try: 278 | mode = input("\nWill you be agent or spymaster?: ") 279 | except KeyboardInterrupt: 280 | print("\nGoodbye!") 281 | break 282 | try: 283 | if mode == "spymaster": 284 | cn.play_agent(reader) 285 | elif mode == "agent": 286 | cn.play_spymaster(reader) 287 | except KeyboardInterrupt: 288 | # Catch interrupts from play functions 289 | pass 290 | 291 | 292 | if __name__ == '__main__': 293 | main() 294 | 295 | -------------------------------------------------------------------------------- /compress.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import numpy as np 4 | import argparse 5 | import tqdm 6 | import re 7 | 8 | parser = argparse.ArgumentParser() 9 | parser.add_argument("vectors", help="Vectors to compress") 10 | parser.add_argument("words", help="The coresponding words, because we do filtering") 11 | parser.add_argument("-n", default=50000, type=int, help="Number of words to include") 12 | parser.add_argument("-f", type=str, nargs='*', help="Take only words from this file") 13 | 14 | def quantize_8bit(data, alpha): 15 | # Normalize data to 0-1 16 | min_val = np.min(data) * alpha 17 | max_val = np.max(data) * alpha 18 | data = data.clip(min_val, max_val) 19 | normalized = (data - min_val) / (max_val - min_val) 20 | # Scale to 0-255 and convert to uint8 21 | quantized = (normalized * 255).astype(np.uint8) 22 | return quantized, min_val, max_val 23 | 24 | def dequantize_8bit(quantized, min_val, max_val): 25 | # Convert back to float range 0-1 26 | normalized = quantized.astype(np.float32) / 255 27 | # Scale back to original range 28 | dequantized = normalized * (max_val - min_val) + min_val 29 | return dequantized 30 | 31 | def main(args): 32 | vecs = np.load(args.vectors) 33 | 34 | n, dim = vecs.shape 35 | if dim != 300: 36 | from sklearn.decomposition import PCA 37 | vecs = PCA(n_components=300).fit_transform(vecs) 38 | 39 | # vecs /= np.linalg.norm(vecs, axis=1, keepdims=True) 40 | with open(args.words) as file: 41 | words = file.readlines() 42 | 43 | if not args.f: 44 | good_words = set(words) 45 | else: 46 | good_words = set() 47 | print(args.f) 48 | for path in args.f: 49 | with open(path) as file: 50 | good_words |= {line.lower().strip() for line in file} 51 | print(f'{len(good_words)=}') 52 | print(list(good_words)[:3]) 53 | 54 | print(len(vecs), len(words)) 55 | assert len(vecs) == len(words) 56 | 57 | included_vectors = [] 58 | included_words = [] 59 | seen = set() 60 | for vec, word in zip(vecs, tqdm.tqdm(words, total=args.n)): 61 | word = word.lower() 62 | word = re.sub('[^a-z0-9]', '', word) 63 | if not word or word.isdigit(): 64 | continue 65 | if word in seen: 66 | continue 67 | if good_words and word not in good_words: 68 | continue 69 | seen.add(word) 70 | included_vectors.append(vec) 71 | included_words.append(word) 72 | if len(included_words) == args.n: 73 | break 74 | 75 | x = np.stack(included_vectors) 76 | 77 | best_alpha, best_err = 0, 1000 78 | for alpha in tqdm.tqdm(np.linspace(x.std()/np.abs(x).max(), 1)): 79 | compressed, min_val, max_val = quantize_8bit(x, alpha) 80 | restored = dequantize_8bit(compressed, min_val, max_val) 81 | err = np.linalg.norm(x - restored, axis=1) / np.linalg.norm(x, axis=1) 82 | #merr = (err**2).mean() 83 | merr = err.mean() 84 | if merr < best_err: 85 | best_err = merr 86 | best_alpha = alpha 87 | print(f"{alpha}, Mean error: {merr}") 88 | print(f"{best_alpha=}") 89 | 90 | compressed, min_val, max_val = quantize_8bit(x, best_alpha) 91 | print("IMPORTANT:") 92 | print(f"min={min_val}, max={max_val}") 93 | 94 | restored = dequantize_8bit(compressed, min_val, max_val) 95 | err = np.linalg.norm(x - restored, axis=1) / np.linalg.norm(x, axis=1) 96 | print(f"Mean error: {err.mean()}") 97 | 98 | data = compressed.tobytes() 99 | print(f"Size: {len(data)/10**6}MB") 100 | 101 | with open(f'{args.vectors}.out', 'wb') as file: 102 | file.write(data) 103 | with open(f'{args.words}.out', 'w') as file: 104 | file.write("\n".join(included_words)) 105 | 106 | if __name__ == '__main__': 107 | main(parser.parse_args()) 108 | -------------------------------------------------------------------------------- /convert.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import tqdm 4 | import sys 5 | import numpy as np 6 | import argparse 7 | 8 | parser = argparse.ArgumentParser(description="Process GloVe dataset.") 9 | parser.add_argument("input", help="Path to the input GloVe text file.") 10 | parser.add_argument("--dim", default=300, help="Expected dimension of each vector") 11 | parser.add_argument("-v", "--output-vectors", help="Path to the output numpy matrix file.", required=True) 12 | parser.add_argument("-w", "--output-words", help="Path to the output words file.", required=True) 13 | args = parser.parse_args() 14 | 15 | matrix = [] 16 | words = [] 17 | with open(args.input, 'r') as inf: 18 | for counter, line in enumerate(tqdm.tqdm(inf)): 19 | word, *rest = line.split() 20 | try: 21 | row = list(map(float, rest)) 22 | except ValueError: 23 | print(f'Bad vector for {repr(word)}. Skipping') 24 | continue 25 | if len(row) != args.dim: 26 | print(f'Bad vector length for {repr(word)}. Skipping') 27 | continue 28 | words.append(word) 29 | matrix.append(np.array(row, dtype=np.float32)) 30 | 31 | np.save(args.output_vectors, np.array(matrix)) 32 | 33 | with open(args.output_words, 'w') as ouf: 34 | ouf.write('\n'.join(words)) 35 | -------------------------------------------------------------------------------- /get_glove.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | mkdir dataset 3 | cd dataset 4 | wget --no-check-certificate http://nlp.stanford.edu/data/glove.6B.zip 5 | unzip glove.6B.zip glove.6B.300d.txt 6 | rm glove.6B.zip 7 | ../convert.py glove.6B.300d.txt -v glove.6B.300d.npy -w words 8 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 14 | 15 | 16 | 17 | 18 | 19 | CODEWORDS 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
38 |
39 |

CODEWORDS

40 | 41 | 44 | 47 | 48 |
49 |
50 |
51 |
52 |
Loading...
53 |
54 |

55 |

See the clues in Statistics

56 |
57 |
58 |
59 |
60 |
61 | 62 | 63 |
64 | 68 | 69 |
70 |
71 |
72 |
73 | CODEWORDS for . 74 |
75 |
76 | Come back tomorrow for a new game, or play 77 | Yesterday's. 78 |
79 |
80 | Go to Today's. 81 |
82 |
83 | Go to Yesterday's. 84 |
85 |

86 | Made by Thomas Dybdahl Ahle. 87 | See the source and contribute on GitHub. 88 |

89 |
90 |
91 | 92 | 134 | 135 | 174 | 175 | 176 | -------------------------------------------------------------------------------- /static/codewords_og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasahle/codenames/484e97211ee520b4befe1c62dfa26e0d4d65903c/static/codewords_og.png -------------------------------------------------------------------------------- /stopwords: -------------------------------------------------------------------------------- 1 | the 2 | , 3 | . 4 | of 5 | to 6 | and 7 | in 8 | a 9 | " 10 | 's 11 | for 12 | - 13 | that 14 | on 15 | is 16 | was 17 | said 18 | with 19 | he 20 | as 21 | it 22 | by 23 | at 24 | ( 25 | ) 26 | from 27 | his 28 | '' 29 | `` 30 | an 31 | be 32 | has 33 | are 34 | have 35 | but 36 | were 37 | not 38 | this 39 | who 40 | they 41 | had 42 | i 43 | which 44 | will 45 | their 46 | : 47 | or 48 | its 49 | one 50 | after 51 | new 52 | been 53 | also 54 | we 55 | would 56 | ' 57 | about 58 | when 59 | there 60 | all 61 | -- 62 | out 63 | other 64 | n't 65 | than 66 | over 67 | into 68 | last 69 | some 70 | $ 71 | you 72 | if 73 | can 74 | do 75 | ; 76 | only 77 | could 78 | us 79 | most 80 | _ 81 | -------------------------------------------------------------------------------- /website/README.md: -------------------------------------------------------------------------------- 1 | python -m http.server 2 | -------------------------------------------------------------------------------- /website/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasahle/codenames/484e97211ee520b4befe1c62dfa26e0d4d65903c/website/favicon.png -------------------------------------------------------------------------------- /website/game.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // Check if the iOS version is at least 17 4 | if (!isIOSVersionAtLeast(16)) { 5 | alert('Requires iOS at least v16, or Android'); 6 | } 7 | 8 | // Start heavy promises 9 | const root = '/website/model'; 10 | const prom = Promise.all([ 11 | //fetchVectors(root + '/vecs.gz'), 12 | fetchVectors(root + '/angel3.gz'), 13 | fetchWordsGz(root + '/words.gz'), 14 | fetchWords(root + '/stopwords') 15 | ]); 16 | const wlprom = fetchWords(root + '/wordlist'); 17 | 18 | const ROWS = 6; 19 | const COLS = 3; 20 | const SECRETS = 6; 21 | const MAX_ROUNDS = 6; 22 | 23 | 24 | function start() { 25 | // This will be today's date or the date from the URL 26 | const date = checkSetAndGetDateHash(); 27 | 28 | // get all data from local storage 29 | const datas = JSON.parse(localStorage.getItem('datas')) || {}; 30 | 31 | if (!(date in datas)) { 32 | // Data prototype 33 | datas[date] = { 34 | board: [], 35 | secret: [], 36 | revealed: [], 37 | hints: [], // Current and previous clues for each round 38 | revealedThisRound: 0, 39 | thinking: false, 40 | roundOver: true, 41 | }; 42 | } 43 | 44 | main(date, datas); 45 | } 46 | 47 | function parseDate(dateStr) { 48 | // By adding time, javascript parses the date as local time, rather than UTC 49 | return new Date(dateStr + 'T00:00'); 50 | } 51 | 52 | function toLongDate(date, includeYear = true) { 53 | const options = { 54 | year: includeYear ? 'numeric' : undefined, // Include year based on the flag 55 | month: 'long', 56 | day: 'numeric' 57 | }; 58 | return parseDate(date).toLocaleDateString(undefined, options); 59 | } 60 | 61 | function toShortDate(date) { 62 | return date.getFullYear() + '-' + 63 | String(date.getMonth() + 1).padStart(2, '0') + '-' + 64 | String(date.getDate()).padStart(2, '0'); 65 | } 66 | 67 | function checkSetAndGetDateHash() { 68 | const dateFromHash = window.location.hash.substring(1); // Remove the '#' character 69 | const dateRegex = /^\d{4}-\d{2}-\d{2}$/; // Simple regex for YYYY-MM-DD format 70 | const today = new Date(); 71 | const inputDate = parseDate(dateFromHash); 72 | 73 | // Check if the date matches the format and is not NaN, and is not in the future 74 | if (!dateFromHash.match(dateRegex) || isNaN(inputDate.getTime()) || inputDate > today) { 75 | const todayStr = toShortDate(today); 76 | window.location.hash = '#' + todayStr; 77 | return todayStr; 78 | } 79 | 80 | return dateFromHash; 81 | } 82 | 83 | // If the user manually changes the hash, we restart 84 | window.addEventListener('hashchange', start); 85 | 86 | function isGameOver(data) { 87 | return getWDL(data) != 0; 88 | } 89 | function isWon(data) { 90 | return getWDL(data) == 1; 91 | } 92 | function isLost(data) { 93 | return getWDL(data) == -1; 94 | } 95 | 96 | function getWDL(data) { 97 | // W=1, L=-1, D=not finished=0 98 | 99 | // Winning is easy 100 | if (data.secret.every(w => data.revealed.includes(w))) 101 | return 1; 102 | 103 | // If we have too many rounds, we've lost for sure. 104 | // Though this shouldn't even happen. 105 | if (data.hints.length > MAX_ROUNDS) 106 | return -1; 107 | 108 | // If we still have rounds to go, we can't have lost, 109 | // so we must still be playing. 110 | if (data.hints.length < MAX_ROUNDS) 111 | return 0; 112 | 113 | // The tricky case is when data.hints == MAX_ROUNDS. 114 | 115 | // Since we don't save the individual guesses per round. 116 | // We have to compute them ourselves. 117 | let guesses = getGuessesPerRound(data.hints, data.revealed, data.secret); 118 | console.log(`Guesses per round: ${guesses}`); 119 | 120 | // We check whether the guesses of the last round contain a mistake. 121 | for (let word of guesses[guesses.length-1]) { 122 | if (!data.secret.includes(word)) 123 | return -1; 124 | } 125 | // If there were no mistakes, (and we haven't won yet) we must still be playing 126 | return 0; 127 | } 128 | 129 | async function main(date, datas) { 130 | console.log(`Starting game with date ${date}.`); 131 | 132 | // Element references 133 | const gameBoard = document.getElementById('gameBoard'); 134 | const clueElem = document.getElementById('clue'); 135 | const roundElem = document.getElementById('round'); 136 | const endTurnButton = document.getElementById('endTurn'); 137 | const remainingCluesSpan = document.getElementById('remainingClues'); 138 | const winLoseText = document.getElementById('win-lose-text'); 139 | 140 | let ai = {}; // matrix, words, stopwords 141 | const data = datas[date]; 142 | 143 | async function init() { 144 | let wordlist = await wlprom; 145 | 146 | // Sample board data from word list, if we don't already have it in the storage. 147 | let usedSaved = false; 148 | if (data.board.length != ROWS * COLS) { 149 | const seed = parseDate(date).getTime() % 2147483647; 150 | console.log(`Sampling new board from seed ${seed}.`); 151 | data.board = sample(seed, wordlist, ROWS*COLS); 152 | data.secret = sample(seed^326236988, data.board, SECRETS); 153 | } else { 154 | console.log(`Using saved board ${data.board}.`); 155 | usedSaved = true; 156 | } 157 | console.log(data); 158 | render(); 159 | 160 | // Wait till we've downloaded the wordvectors before we start the round. 161 | // Would it be better to cache the ai to localStorage? 162 | // It seems unnecessary as the browswer does its own caching. 163 | const [matrix, words, stopwords] = await prom; 164 | ai = {matrix, words, stopwords}; 165 | 166 | if (isGameOver(data)) { 167 | console.log("Not starting new round, because game is already over."); 168 | } 169 | else if (!data.roundOver && !data.thinking) { 170 | console.log("Not starting new round, because we already seem to be in a round."); 171 | } 172 | else { 173 | newRound(); 174 | } 175 | } 176 | 177 | function render() { 178 | // Since we call render every time we change something, 179 | // this is a good time to save the state 180 | localStorage.setItem('datas', JSON.stringify(datas)); 181 | 182 | // Render board 183 | if (data.board.length > 0) { 184 | gameBoard.innerHTML = ''; // Clear existing board 185 | gameBoard.style.gridTemplateColumns = `repeat(${COLS}, 1fr)`; 186 | data.board.forEach(word => { 187 | const cardElement = document.createElement('div'); 188 | cardElement.className = 'card'; 189 | 190 | const cardSpan = document.createElement('span'); 191 | cardSpan.textContent = word; 192 | cardElement.appendChild(cardSpan); 193 | 194 | cardElement.onclick = () => handleCardClick(word); 195 | 196 | if (data.revealed.includes(word)) { 197 | cardElement.classList.add(data.secret.includes(word) ? "good" : "bad"); 198 | cardElement.classList.add("revealed"); 199 | } 200 | 201 | gameBoard.appendChild(cardElement); 202 | 203 | // Reduce size if too large 204 | if (cardElement.scrollWidth > cardElement.clientWidth) { 205 | cardElement.style.fontSize = "80%"; 206 | } 207 | }); 208 | } 209 | 210 | // Set Round indicator 211 | roundElem.textContent = `Round ${data.hints.length} / ${MAX_ROUNDS}`; 212 | 213 | // Set "n words remaining" text 214 | if (data.hints.length != 0) { 215 | const {clue, n} = data.hints[data.hints.length-1]; 216 | let r = n - data.revealedThisRound; 217 | let s = `pick ${r} more word`; 218 | if (r != 1) 219 | s += "s"; 220 | remainingCluesSpan.textContent = s; 221 | } 222 | document.body.classList.toggle("round-over", data.roundOver); 223 | document.body.classList.toggle("game-over", isGameOver(data)); 224 | document.body.classList.toggle("game-lost", isLost(data)); 225 | document.body.classList.toggle("game-win", isWon(data)); 226 | document.body.classList.toggle("today", parseDate(date).toDateString() === new Date().toDateString()); 227 | 228 | // Clues 229 | if (!ai) { 230 | clueElem.textContent = "Loading..."; 231 | console.log('loading...'); 232 | } 233 | else if (data.thinking) { 234 | clueElem.textContent = "Thinking..."; 235 | } 236 | else if (isGameOver(data)) { 237 | let got = data.secret.filter(w => data.revealed.includes(w)).length; 238 | winLoseText.innerHTML = isWon(data) 239 | ? "Hurray! You Won!" 240 | : `You got ${got} out of ${SECRETS}.`; 241 | } 242 | else if (data.hints.length != 0) { 243 | const {clue, n} = data.hints[data.hints.length-1]; 244 | clueElem.innerHTML = `Clue: ${clue.toUpperCase()} ${n}`; 245 | } 246 | 247 | // Footer 248 | for (const span of document.getElementsByClassName("date")) { 249 | span.textContent = toLongDate(date); 250 | } 251 | } 252 | 253 | function handleCardClick(word) { 254 | console.log("Card clicked:", word); 255 | if (data.roundOver) { 256 | // Shouldn't be possible to get here 257 | console.log("Round over, please start next round."); 258 | return; 259 | } 260 | if (data.thinking) { 261 | console.log("Please wait..."); 262 | return; 263 | } 264 | if (data.revealed.includes(word)) { 265 | return; 266 | } 267 | 268 | // Get the last hint that the user saw before clicking 269 | const {clue, n} = data.hints[data.hints.length-1]; 270 | 271 | gtag('event', 'card_click', { 272 | 'event_category': 'Game Interaction', 273 | 'event_label': 'Card Clicked', 274 | // The hint 275 | 'clue': clue, 276 | 'clue_n': n, 277 | // The interaction 278 | 'clicked': word, 279 | 'was_correct': data.secret.includes(word) ? 1 : 0, 280 | // The board 281 | 'secret': data.secret.join(','), 282 | 'board': data.board.join(','), 283 | 'revealed': data.revealed, 284 | // Extra data 285 | 'hints': JSON.stringify(data.hints), 286 | }); 287 | 288 | data.revealed.push(word); 289 | data.revealedThisRound += 1; 290 | 291 | if (data.revealedThisRound == n) { 292 | data.roundOver = true; 293 | } 294 | else if (!data.secret.includes(word)) { 295 | data.roundOver = true; 296 | } 297 | 298 | console.log(`Is game over? ${isGameOver(data)} ${getWDL(data)}`); 299 | if (isGameOver(data)) { 300 | onGameOver(); 301 | } 302 | 303 | render(); 304 | } 305 | 306 | function newRound() { 307 | if (isGameOver(data)) { 308 | console.log("Can't start new round. Game is over."); 309 | return; 310 | } 311 | 312 | data.revealedThisRound = 0; 313 | data.roundOver = false; 314 | 315 | data.thinking = true; 316 | render(); 317 | 318 | const board = data.board.filter(w => !data.revealed.includes(w)); 319 | const secret = data.secret.filter(w => !data.revealed.includes(w)); 320 | let stopwords = [...ai.stopwords]; 321 | // Don't repeat hints 322 | for (let hint of data.hints) { 323 | stopwords.push(hint.clue); 324 | } 325 | 326 | let agg = 0.6; // Default aggressiveness = 0.6 327 | let shift = .9; 328 | 329 | // Let's adjust aggressiveness based on how well the user is doing 330 | // let got = data.secret.filter(w => data.revealed.includes(w)).length; 331 | // let success = got / data.revealed.length; 332 | // if (success <= .5) 333 | // agg = 0.3; 334 | 335 | // If this is the last round, we need to give a clue number high enough 336 | // that the user has a change to win. 337 | if (data.hints.length == MAX_ROUNDS - 1) { 338 | shift = .99; 339 | agg = 100; 340 | } 341 | const hint = makeHint(ai.matrix, ai.words, stopwords, board, secret, agg, shift); 342 | data.hints.push(hint); 343 | 344 | data.thinking = false; 345 | render(); 346 | } 347 | 348 | endTurnButton.onclick = function() { 349 | if (data.roundOver) { 350 | newRound(); 351 | } else { 352 | console.log("Just finish the round yourself."); 353 | } 354 | } 355 | 356 | for (const link of document.getElementsByClassName("yesterdays-link")) { 357 | console.log(link); 358 | link.onclick = function() { 359 | const curDate = parseDate(date); 360 | curDate.setDate(curDate.getDate() - 1); // Subtract one day 361 | const yesterday = toShortDate(curDate); 362 | window.location.hash = '#' + yesterday; 363 | }; 364 | } 365 | 366 | for (const link of document.getElementsByClassName("todays-link")) { 367 | link.onclick = function() { 368 | window.location.hash = '#'; 369 | }; 370 | } 371 | 372 | function onGameOver() { 373 | if (!isGameOver(data)) { 374 | console.log("Error: Game is not over."); 375 | return; 376 | } 377 | 378 | gtag('event', 'game_result', { 379 | 'event_category': 'Game Interaction', 380 | 'event_label': isWon(data) ? 'Win' : 'Lost', 381 | 'value': isWon(data) ? 1 : 0 382 | }); 383 | 384 | // Ideally we should re-render the statistics here. 385 | // We can't actually do this with our design. 386 | // But it's ok, because the game can't end while 387 | // the statistics are open. 388 | } 389 | 390 | init(); 391 | initMenu(date, datas); 392 | } 393 | 394 | function initMenu(date, allGameDatas) { 395 | const statsButton = document.getElementById('stats-button'); 396 | const statsLink = document.getElementById('stats-link'); 397 | const helpButton = document.getElementById('help-button'); 398 | const statsClose = document.getElementById('stats-close'); 399 | const helpClose = document.getElementById('help-close'); 400 | const statsModal = document.getElementById('stats-modal'); 401 | const helpModal = document.getElementById('help-modal'); 402 | const analysisDiv = document.getElementById('post-game-analysis'); 403 | const shareButton = document.getElementById('shareButton'); 404 | 405 | const data = { 406 | helpShown: false, 407 | statsShown: false, 408 | } 409 | const gameData = allGameDatas[date]; 410 | 411 | statsButton.onclick = function() { 412 | data.statsShown = true; 413 | render(); 414 | } 415 | 416 | statsLink.onclick = function() { 417 | data.statsShown = true; 418 | render(); 419 | } 420 | 421 | statsClose.onclick = closeAll; 422 | 423 | helpButton.onclick = function() { 424 | data.helpShown = true; 425 | render(); 426 | } 427 | 428 | helpClose.onclick = closeAll; 429 | 430 | for (const modal of document.getElementsByClassName("modal")) { 431 | // Only react on direct clicks, so we don't close the modal when 432 | // clicking in modal-inner. 433 | modal.onclick = function(event) { 434 | if (event.target === modal) { 435 | closeAll(); 436 | } 437 | }; 438 | } 439 | 440 | document.addEventListener('keydown', (event) => { 441 | if (event.key === 'Escape') { 442 | closeAll(); 443 | } 444 | }); 445 | 446 | function closeAll() { 447 | data.statsShown = false; 448 | data.helpShown = false; 449 | render(); 450 | } 451 | 452 | shareButton.onclick = function() { 453 | 454 | const {shareString, logString} = compileLog(gameData.hints, gameData.revealed, gameData.secret); 455 | let used = isWon(gameData) ? gameData.hints.length : "X"; 456 | let copyString = `CODEWORDS ${toLongDate(date, false)}, ${used}/${MAX_ROUNDS}`; 457 | copyString += "\n\n" + shareString; 458 | 459 | // Copying the string to the clipboard 460 | navigator.clipboard.writeText(copyString).then(function() { 461 | // Show the message 462 | const message = document.getElementById('message'); 463 | message.style.display = 'inline'; 464 | 465 | // Hide the message after 2 seconds 466 | setTimeout(function() { 467 | message.style.display = 'none'; 468 | }, 2000); 469 | }).catch(function(error) { 470 | // Handle any errors here 471 | console.error('Error copying text: ', error); 472 | }); 473 | } 474 | 475 | function render() { 476 | statsModal.style.display = data.statsShown ? "block" : "none"; 477 | helpModal.style.display = data.helpShown ? "block" : "none"; 478 | 479 | // Get stats 480 | const played = Object.keys(allGameDatas).filter(d => isGameOver(allGameDatas[d])).length; 481 | const wins = Object.keys(allGameDatas).filter(d => isWon(allGameDatas[d])).length; 482 | document.getElementById('playedCount').textContent = played; 483 | let winPercentage = played > 0 ? Math.round(wins / played * 100) : 0; 484 | document.getElementById('winPercentage').textContent = winPercentage; 485 | document.getElementById('winPercentage').setAttribute("title", `Won ${wins} / ${played}`); 486 | 487 | // Compute streaks from dates 488 | const {longestStreak, longestEnd, currentStreak} = findStreaks(allGameDatas); 489 | document.getElementById('currentStreak').textContent = currentStreak; 490 | document.getElementById('maxStreak').textContent = longestStreak; 491 | if (longestEnd !== null && longestEnd !== undefined) 492 | document.getElementById('maxStreak').setAttribute("title", `Streak ended ${toShortDate(longestEnd)}`); 493 | 494 | // Compute guess distribution 495 | const distribution = new Array(MAX_ROUNDS).fill(0); 496 | for (const d of Object.values(allGameDatas)) { 497 | if (isWon(d)) { 498 | distribution[d.hints.length - 1]++; 499 | } 500 | } 501 | const guessDistributionContainer = document.getElementById('guessDistribution'); 502 | guessDistributionContainer.innerHTML = ''; 503 | distribution.forEach((count, index) => { 504 | const barContainer = document.createElement('div'); 505 | barContainer.className = 'guessBar-container'; 506 | barContainer.innerHTML = `
${index+1}
`; 507 | const bar = document.createElement('div'); 508 | bar.className = 'guessBar'; 509 | const percent = count / Math.max(...distribution) * 100; 510 | const width = count > 0 ? `${percent.toFixed(2)}%` : '0%'; 511 | bar.style.width = `calc(max(1rem, ${width}))`; 512 | bar.innerHTML = `${count}`; 513 | barContainer.appendChild(bar); 514 | guessDistributionContainer.appendChild(barContainer); 515 | }); 516 | 517 | // Log of clues 518 | if (isGameOver(gameData)) { 519 | console.log("Making log"); 520 | const {shareString, logString} = compileLog(gameData.hints, gameData.revealed, gameData.secret); 521 | analysisDiv.innerHTML = logString; 522 | } else { 523 | console.log("Making log"); 524 | analysisDiv.innerHTML = "

Come back here after the game.

"; 525 | } 526 | } 527 | 528 | // Show help on first visit 529 | if (localStorage.getItem('hasVisited') === null) { 530 | data.helpShown = true; 531 | render(); 532 | localStorage.setItem('hasVisited', 'true'); 533 | } 534 | } 535 | 536 | function fetchWords(path) { 537 | return fetch(path) 538 | .then(response => response.text()) 539 | .then(text => text.split('\n').filter(word => word.trim().length > 0)) 540 | .catch(error => { 541 | console.error('Error fetching or processing the file:', error); 542 | }); 543 | } 544 | 545 | function fetchWordsGz(path) { 546 | return fetch(path) 547 | .then(response => response.body) 548 | .then(stream => { 549 | const decompressionStream = new DecompressionStream('gzip'); 550 | const decompressedStream = stream.pipeThrough(decompressionStream); 551 | return new Response(decompressedStream).text(); 552 | }) 553 | .then(text => text.split('\n')) 554 | .catch(error => { 555 | console.error('Error fetching or processing the file:', error); 556 | }); 557 | } 558 | 559 | function fetchVectors(path) { 560 | console.log(`Loading model ${path}`); 561 | return fetch(path) 562 | .then(response => response.body) 563 | .then(stream => { 564 | const decompressionStream = new DecompressionStream('gzip'); 565 | const decompressedStream = stream.pipeThrough(decompressionStream); 566 | return new Response(decompressedStream).arrayBuffer(); 567 | }) 568 | .then(decompressedBuffer => { 569 | const dim = 300; 570 | const rows = 9910; 571 | const byteArray = new Uint8Array(decompressedBuffer); 572 | 573 | // Glove: 574 | // avg 5.77 575 | // win@6 0.67 576 | //const min_val=-2.645588700353074; 577 | //const max_val=2.6333964024164196; 578 | 579 | // Angel, PCA: 580 | // avg 5.62 - 5.68 581 | // win@6 0.688 - 0.696 582 | // Avg: 1.980 - 2.002 583 | // const min_val=-3.508529352673804; 584 | // const max_val=4.6301482913369485; 585 | 586 | // Patched, angel2, lora 587 | // avg 5.5 588 | // win@6 0.714 589 | // Avg: 1.991 590 | //const min_val=-6.580836296081543; 591 | //const max_val=8.107464790344238; 592 | 593 | // angel3 594 | // avg game 5.37 595 | // win@6 0.765 596 | // Avg clue: 1.883 597 | const min_val=-1.7025203704833984; 598 | const max_val=1.5609053373336792; 599 | 600 | // Dequantize 601 | const quantizedMatrix = mlMatrix.Matrix.from1DArray(rows, dim, byteArray); 602 | let matrix = quantizedMatrix.div(255).mul(max_val - min_val).add(min_val); 603 | 604 | // Normalize 605 | for (let i = 0; i < matrix.rows; i++) { 606 | let row = matrix.getRow(i); 607 | let norm = Math.sqrt(row.reduce((sum, value) => sum + value * value, 0)); 608 | matrix.setRow(i, row.map(value => value / norm)); 609 | } 610 | return matrix; 611 | }) 612 | .catch(error => console.error('Error loading file:', error)); 613 | } 614 | 615 | function sample(key, originalArray, n) { 616 | // Certified random coefficients from random.org 617 | const cs = [82304423, 346724810, 725211102, 50719932, 978969693, 1594878607]; 618 | 619 | // Polynomial random generator 620 | function next() { 621 | let result = cs[0]; 622 | for (let i = 1; i < cs.length; i++) { 623 | result = result * key + cs[i]; 624 | result %= 2147483647; 625 | } 626 | key += 1; // Increment the key for the next call 627 | return result; 628 | } 629 | 630 | const array = [...originalArray]; 631 | for (let i = 0; i < Math.min(n, array.length); i++) { 632 | const j = i + next() % (array.length - i); 633 | [array[i], array[j]] = [array[j], array[i]]; 634 | } 635 | return array.slice(0, n) 636 | } 637 | 638 | 639 | function findVector(words, word) { 640 | let index = words.indexOf(word.toLowerCase()); 641 | if (index == -1) { 642 | console.log(`Can't find ${word}`); 643 | index = 0; 644 | } 645 | return index; 646 | } 647 | 648 | function makeHint(matrix, words, stopwords, board, secret, aggressiveness, shift) { 649 | /* The algorithm uses the following formula for scoring clues: 650 | * gap * (n^agg - shift) 651 | * Where `gap` is the gap in inner products between the worst "good" word 652 | * and the best "bad" word. 653 | * `n` is the size of the clue, and agg is the aggressiveness. 654 | * 655 | * So if agg = 0, we only look at the `gap`. 656 | * If agg = inf, we only care about `n`. 657 | * Default agg should be around 0.6. 658 | */ 659 | console.log("Thinking..."); 660 | 661 | const avoids = board.filter(word => !secret.includes(word)); 662 | console.log(avoids); 663 | const badVectors = new mlMatrix.MatrixRowSelectionView(matrix, 664 | avoids.map(word => findVector(words, word))); 665 | const goodVectors = new mlMatrix.MatrixRowSelectionView(matrix, 666 | secret.map(word => findVector(words, word))); 667 | 668 | // For any clue (row) vector, we want to find the largest IP with a bad 669 | // vector, since since that sets a lower bound on the IPs we are willing 670 | // to accept. This comes from the "zero bad vectors accepted" requirement. 671 | const nm = matrix.mmul(badVectors.transpose()).max('row'); 672 | // For the good vectors, we want to know the IP with each one, so this is 673 | // a matrix of shape |words| x |secret| 674 | const pm = matrix.mmul(goodVectors.transpose()); 675 | 676 | let best = {}; 677 | for (let step = 0; step < words.length; step++) { 678 | const clue = words[step]; 679 | let lowerBound = Math.max(nm[step] || 0, 0); 680 | const scores = pm.getRow(step); 681 | 682 | // TODO: Maybe sometimes it's OK to include a single bad word with a high score, 683 | // if the `n` is large enough? 684 | // Could test this with GPT. 685 | 686 | // If the best score is lower than the lower bound, there is no reason 687 | // to even try it. 688 | if (stopwords.includes(clue)) { 689 | continue; 690 | } 691 | // Don't use something directly present on the board 692 | let skip = false; 693 | for (const word of board) { 694 | skip = skip || word.includes(clue.toUpperCase()); 695 | skip = skip || clue.toUpperCase().includes(word); 696 | } 697 | if (skip) { 698 | continue; 699 | } 700 | 701 | // Order scores by highest to lowest inner product with the clue. 702 | const ss = scores 703 | .map((score, i) => ({ score, index: i })) 704 | .sort((a, b) => b.score - a.score); 705 | 706 | for (let j = 0; j < ss.length; j++) { 707 | const gap = ss[j].score - lowerBound; 708 | 709 | // Save that we can achieve gap s-lowerBound with n=j+1 710 | if (!best[j] || gap > best[j].gap) { 711 | best[j] = {gap, clue, scores, lowerBound}; 712 | } 713 | } 714 | } 715 | 716 | let combinedBest = {combinedScore: -10000}; 717 | for (let [n, {gap, clue, scores, lowerBound}] of Object.entries(best)) { 718 | n = parseInt(n); 719 | console.log(`N: ${n+1}, Gap: ${gap}, Clue: ${clue}, Lb: ${lowerBound}`); 720 | console.log(scores); 721 | let combinedScore = gap * (Math.pow(n+1, aggressiveness) - shift); 722 | console.log(`Combined Score: ${combinedScore}`); 723 | 724 | let indices = [...scores.keys()]; 725 | indices.sort((a, b) => scores[b] - scores[a]); 726 | let largestIndices = indices.slice(0, n+1); 727 | let intendedClues = largestIndices.map(i => secret[i]); 728 | 729 | if (combinedScore > combinedBest.combinedScore) { 730 | combinedBest = {n: n+1, clue, intendedClues, combinedScore}; 731 | } 732 | } 733 | 734 | return combinedBest; 735 | } 736 | 737 | function getGuessesPerRound(hints, revealed, secret) { 738 | let j = 0; 739 | const result = []; 740 | for (const hint of hints) { 741 | const guesses = []; 742 | while ( 743 | j !== revealed.length // Not through all actions yet. 744 | && secret.includes(revealed[j]) // No mistake yet. 745 | && guesses.length != hint.n // Haven't finished by success. 746 | ) { 747 | guesses.push(revealed[j]); 748 | j++; 749 | } 750 | // Maybe add a mistake 751 | if (j !== revealed.length && guesses.length != hint.n) { 752 | guesses.push(revealed[j]); 753 | j++; 754 | } 755 | result.push(guesses); 756 | } 757 | // If we just started a new round 758 | if (result.length == hints.length-1) { 759 | result.push([]); 760 | } 761 | if (result.length != hints.length) { 762 | console.log(`Bad length ${result.length}, ${hints.length}.`); 763 | } 764 | return result; 765 | 766 | } 767 | 768 | function compileLog(hints, revealed, secret) { 769 | let shareString = ""; 770 | let s = "
    "; 771 | const guesses = getGuessesPerRound(hints, revealed, secret); 772 | for (let i = 0; i < hints.length; i++) { 773 | let hint = hints[i]; 774 | s += `
  1. Round ${i + 1} Clue: ${hint.clue.toUpperCase()} ${hint.n}

    `; 775 | s += "
    "; 776 | 777 | let guessed = guesses[i]; 778 | 779 | function href(word) { 780 | return `href="https://www.google.com/search?q=${hint.clue}+${word}"` 781 | } 782 | 783 | let intended = hint.intendedClues; 784 | for (let word of intended) { 785 | if (!guessed.includes(word)) { 786 | s += `${word}(Intended clue)`; 787 | shareString += "⬜"; // White box 788 | } 789 | } 790 | for (let word of guessed) { 791 | if (intended.includes(word)) { 792 | s += `${word}(Guessed and Intended)`; 793 | shareString += "🟨"; // Orange box 794 | } else if (secret.includes(word)) { 795 | s += `${word}(Guessed by chance)`; 796 | shareString += "🟨"; // Orange box 797 | } else { 798 | s += `${word}(Incorrect)`; 799 | shareString += "⬛"; // Black box 800 | } 801 | } 802 | shareString += "\n"; 803 | 804 | s += "
  2. "; 805 | } 806 | s += "
"; 807 | return {shareString, logString: s}; 808 | } 809 | 810 | function findStreaks(datesObject) { 811 | // Filter the object keys based on isGameOver, then convert to date objects and sort them 812 | const dates = Object.keys(datesObject) 813 | .filter(key => isGameOver(datesObject[key])) 814 | .map(date => parseDate(date)) 815 | .sort((a, b) => a - b); 816 | 817 | if (dates.length == 0) { 818 | return { 819 | longestStreak: 0, 820 | longestEnd: null, 821 | currentStreak: 0 822 | }; 823 | } 824 | 825 | let longestStreak = 0; 826 | let currentStreak = 0; 827 | let longestEnd = null; 828 | let tempStreak = 1; 829 | 830 | for (let i = 1; i < dates.length; i++) { 831 | // Check if the current date is consecutive 832 | // Apparently this is OK in Javascript without leap-second issues(?) 833 | if (dates[i] - dates[i - 1] === 86400000) { // 86400000ms = 1 day 834 | tempStreak++; 835 | } else { 836 | // Update the longest streak if needed 837 | if (tempStreak > longestStreak) { 838 | longestStreak = tempStreak; 839 | longestEnd = dates[i - 1]; 840 | } 841 | tempStreak = 1; 842 | } 843 | } 844 | 845 | // Check the last streak 846 | if (tempStreak > longestStreak) { 847 | longestStreak = tempStreak; 848 | longestEnd = dates[dates.length - 1]; 849 | } 850 | 851 | // Determine the current streak 852 | let today = new Date(); 853 | today.setHours(0, 0, 0, 0); // Normalize today's date 854 | 855 | while (dates.filter((d) => d - today == 0).length == 1) { 856 | currentStreak++; 857 | today.setDate(today.getDate() - 1); 858 | } 859 | 860 | return { 861 | longestStreak, 862 | longestEnd, 863 | currentStreak 864 | }; 865 | } 866 | 867 | // Utils 868 | function isIOSVersionAtLeast(version) { 869 | const ua = window.navigator.userAgent; 870 | const ios = ua.match(/OS (\d+)_/); 871 | 872 | if (ios && ios.length > 1) { 873 | const iosVersion = parseInt(ios[1], 10); 874 | return iosVersion >= version; 875 | } 876 | 877 | // If not iOS, we are fine 878 | return true; 879 | } 880 | -------------------------------------------------------------------------------- /website/model/angel.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasahle/codenames/484e97211ee520b4befe1c62dfa26e0d4d65903c/website/model/angel.gz -------------------------------------------------------------------------------- /website/model/angel2.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasahle/codenames/484e97211ee520b4befe1c62dfa26e0d4d65903c/website/model/angel2.gz -------------------------------------------------------------------------------- /website/model/angel3.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasahle/codenames/484e97211ee520b4befe1c62dfa26e0d4d65903c/website/model/angel3.gz -------------------------------------------------------------------------------- /website/model/stopwords: -------------------------------------------------------------------------------- 1 | the 2 | 3 | , 4 | . 5 | of 6 | to 7 | and 8 | in 9 | a 10 | " 11 | 's 12 | for 13 | - 14 | that 15 | on 16 | is 17 | was 18 | said 19 | with 20 | he 21 | as 22 | it 23 | by 24 | at 25 | ( 26 | ) 27 | from 28 | his 29 | '' 30 | `` 31 | an 32 | be 33 | has 34 | are 35 | have 36 | but 37 | were 38 | not 39 | this 40 | who 41 | they 42 | had 43 | i 44 | which 45 | will 46 | their 47 | : 48 | or 49 | its 50 | one 51 | after 52 | new 53 | been 54 | also 55 | we 56 | would 57 | ' 58 | about 59 | when 60 | there 61 | all 62 | -- 63 | out 64 | other 65 | n't 66 | than 67 | over 68 | into 69 | last 70 | some 71 | $ 72 | you 73 | if 74 | can 75 | do 76 | ; 77 | only 78 | could 79 | us 80 | most 81 | _ 82 | jews 83 | horny 84 | ebony 85 | sexual 86 | bestiality 87 | racist 88 | racism 89 | rape 90 | monkey 91 | cock 92 | loose 93 | hell 94 | butt 95 | queer 96 | damn 97 | sperm 98 | booty 99 | crow 100 | lesbian 101 | jerk 102 | breast 103 | prick 104 | trash 105 | gay 106 | sex 107 | hag 108 | gypsy 109 | spank 110 | swine 111 | fart 112 | jew 113 | tart 114 | grope 115 | bastard 116 | bang 117 | pig 118 | spook 119 | homosexual 120 | shit 121 | knob 122 | suck 123 | snatch 124 | slit 125 | nipple 126 | tramp 127 | ape 128 | retard 129 | bum 130 | trey 131 | sexist 132 | -------------------------------------------------------------------------------- /website/model/vecs.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasahle/codenames/484e97211ee520b4befe1c62dfa26e0d4d65903c/website/model/vecs.gz -------------------------------------------------------------------------------- /website/model/vecs.gz.old: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasahle/codenames/484e97211ee520b4befe1c62dfa26e0d4d65903c/website/model/vecs.gz.old -------------------------------------------------------------------------------- /website/model/wordlist: -------------------------------------------------------------------------------- 1 | AFRICA 2 | AGENT 3 | AIR 4 | ALIEN 5 | ALPS 6 | AMAZON 7 | AMBULANCE 8 | AMERICA 9 | ANGEL 10 | ANTARCTICA 11 | APPLE 12 | ARM 13 | ATLANTIS 14 | AUSTRALIA 15 | AZTEC 16 | BACK 17 | BALL 18 | BAND 19 | BANK 20 | BAR 21 | BARK 22 | BAT 23 | BATTERY 24 | BEACH 25 | BEAR 26 | BEAT 27 | BED 28 | BEIJING 29 | BELL 30 | BELT 31 | BERLIN 32 | BERMUDA 33 | BERRY 34 | BILL 35 | BLOCK 36 | BOARD 37 | BOLT 38 | BOMB 39 | BOND 40 | BOOM 41 | BOOT 42 | BOTTLE 43 | BOW 44 | BOX 45 | BRIDGE 46 | BRUSH 47 | BUCK 48 | BUFFALO 49 | BUG 50 | BUGLE 51 | BUTTON 52 | CALF 53 | CANADA 54 | CAP 55 | CAPITAL 56 | CAR 57 | CARD 58 | CARROT 59 | CASINO 60 | CAST 61 | CAT 62 | CELL 63 | CENTAUR 64 | CENTER 65 | CHAIR 66 | CHANGE 67 | CHARGE 68 | CHECK 69 | CHEST 70 | CHICK 71 | CHINA 72 | CHOCOLATE 73 | CHURCH 74 | CIRCLE 75 | CLIFF 76 | CLOAK 77 | CLUB 78 | CODE 79 | COLD 80 | COMIC 81 | COMPOUND 82 | CONCERT 83 | CONDUCTOR 84 | CONTRACT 85 | COOK 86 | COPPER 87 | COTTON 88 | COURT 89 | COVER 90 | CRANE 91 | CRASH 92 | CRICKET 93 | CROSS 94 | CROWN 95 | CYCLE 96 | CZECH 97 | DANCE 98 | DATE 99 | DAY 100 | DEATH 101 | DECK 102 | DEGREE 103 | DIAMOND 104 | DICE 105 | DINOSAUR 106 | DISEASE 107 | DOCTOR 108 | DOG 109 | DRAFT 110 | DRAGON 111 | DRESS 112 | DRILL 113 | DROP 114 | DUCK 115 | DWARF 116 | EAGLE 117 | EGYPT 118 | EMBASSY 119 | ENGINE 120 | ENGLAND 121 | EUROPE 122 | EYE 123 | FACE 124 | FAIR 125 | FALL 126 | FAN 127 | FENCE 128 | FIELD 129 | FIGHTER 130 | FIGURE 131 | FILE 132 | FILM 133 | FIRE 134 | FISH 135 | FLUTE 136 | FLY 137 | FOOT 138 | FORCE 139 | FOREST 140 | FORK 141 | FRANCE 142 | GAME 143 | GAS 144 | GENIUS 145 | GERMANY 146 | GHOST 147 | GIANT 148 | GLASS 149 | GLOVE 150 | GOLD 151 | GRACE 152 | GRASS 153 | GREECE 154 | GREEN 155 | GROUND 156 | HAM 157 | HAND 158 | HAWK 159 | HEAD 160 | HEART 161 | HELICOPTER 162 | HIMALAYAS 163 | HOLE 164 | HOLLYWOOD 165 | HONEY 166 | HOOD 167 | HOOK 168 | HORN 169 | HORSE 170 | HORSESHOE 171 | HOSPITAL 172 | HOTEL 173 | ICE 174 | INDIA 175 | IRON 176 | IVORY 177 | JACK 178 | JAM 179 | JET 180 | JUPITER 181 | KANGAROO 182 | KETCHUP 183 | KEY 184 | KID 185 | KING 186 | KIWI 187 | KNIFE 188 | KNIGHT 189 | LAB 190 | LAP 191 | LASER 192 | LAWYER 193 | LEAD 194 | LEMON 195 | LEPRECHAUN 196 | LIFE 197 | LIGHT 198 | LIMOUSINE 199 | LINE 200 | LINK 201 | LION 202 | LITTER 203 | LOCK 204 | LOG 205 | LONDON 206 | LUCK 207 | MAIL 208 | MAMMOTH 209 | MAPLE 210 | MARBLE 211 | MARCH 212 | MASS 213 | MATCH 214 | MERCURY 215 | MEXICO 216 | MICROSCOPE 217 | MILLIONAIRE 218 | MINE 219 | MINT 220 | MISSILE 221 | MODEL 222 | MOLE 223 | MOON 224 | MOSCOW 225 | MOUNT 226 | MOUSE 227 | MOUTH 228 | MUG 229 | NAIL 230 | NEEDLE 231 | NET 232 | NIGHT 233 | NINJA 234 | NOTE 235 | NOVEL 236 | NURSE 237 | NUT 238 | OCTOPUS 239 | OIL 240 | OLIVE 241 | OLYMPUS 242 | OPERA 243 | ORANGE 244 | ORGAN 245 | PALM 246 | PAN 247 | PANTS 248 | PAPER 249 | PARACHUTE 250 | PARK 251 | PART 252 | PASS 253 | PASTE 254 | PENGUIN 255 | PHOENIX 256 | PIANO 257 | PIE 258 | PILOT 259 | PIN 260 | PIPE 261 | PIRATE 262 | PISTOL 263 | PIT 264 | PITCH 265 | PLANE 266 | PLASTIC 267 | PLATE 268 | PLATYPUS 269 | PLAY 270 | PLOT 271 | POINT 272 | POISON 273 | POLE 274 | POLICE 275 | POOL 276 | PORT 277 | POST 278 | POUND 279 | PRESS 280 | PRINCESS 281 | PUMPKIN 282 | PUPIL 283 | PYRAMID 284 | QUEEN 285 | RABBIT 286 | RACKET 287 | RAY 288 | REVOLUTION 289 | RING 290 | ROBIN 291 | ROBOT 292 | ROCK 293 | ROME 294 | ROOT 295 | ROSE 296 | ROULETTE 297 | ROUND 298 | ROW 299 | RULER 300 | SATELLITE 301 | SATURN 302 | SCALE 303 | SCHOOL 304 | SCIENTIST 305 | SCORPION 306 | SCREEN 307 | SCUBA 308 | SEAL 309 | SERVER 310 | SHADOW 311 | SHAKESPEARE 312 | SHARK 313 | SHIP 314 | SHOE 315 | SHOP 316 | SHOT 317 | SINK 318 | SKYSCRAPER 319 | SLIP 320 | SLUG 321 | SMUGGLER 322 | SNOW 323 | SNOWMAN 324 | SOCK 325 | SOLDIER 326 | SOUL 327 | SOUND 328 | SPACE 329 | SPELL 330 | SPIDER 331 | SPIKE 332 | SPINE 333 | SPOT 334 | SPRING 335 | SPY 336 | SQUARE 337 | STADIUM 338 | STAFF 339 | STAR 340 | STATE 341 | STICK 342 | STOCK 343 | STRAW 344 | STREAM 345 | STRIKE 346 | STRING 347 | SUB 348 | SUIT 349 | SUPERHERO 350 | SWING 351 | SWITCH 352 | TABLE 353 | TABLET 354 | TAG 355 | TAIL 356 | TAP 357 | TEACHER 358 | TELESCOPE 359 | TEMPLE 360 | THEATER 361 | THIEF 362 | THUMB 363 | TICK 364 | TIE 365 | TIME 366 | TOKYO 367 | TOOTH 368 | TORCH 369 | TOWER 370 | TRACK 371 | TRAIN 372 | TRIANGLE 373 | TRIP 374 | TRUNK 375 | TUBE 376 | TURKEY 377 | UNDERTAKER 378 | UNICORN 379 | VACUUM 380 | VAN 381 | VET 382 | WAKE 383 | WALL 384 | WAR 385 | WASHER 386 | WASHINGTON 387 | WATCH 388 | WATER 389 | WAVE 390 | WEB 391 | WELL 392 | WHALE 393 | WHIP 394 | WIND 395 | WITCH 396 | WORM 397 | YARD 398 | -------------------------------------------------------------------------------- /website/model/words.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasahle/codenames/484e97211ee520b4befe1c62dfa26e0d4d65903c/website/model/words.gz -------------------------------------------------------------------------------- /website/styles.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --good-color: #ebca47; 3 | --bad-color: #5c4f4f; 4 | --card-color: #efede9; 5 | --card-light: #f5f4f3; 6 | --bad-light: #534846; 7 | --good-light: #f4d146; 8 | } 9 | 10 | * { 11 | margin: 0; 12 | padding: 0; 13 | box-sizing: border-box; 14 | } 15 | 16 | body { 17 | margin-bottom: 5rem; 18 | font-family: "Helvetica Neue", sans-serif; 19 | } 20 | 21 | a { 22 | text-decoration: none; 23 | color: black; 24 | } 25 | 26 | body.round-over { 27 | background: #faf8f8; 28 | } 29 | 30 | body.game-win { 31 | background: #fffae6; 32 | } 33 | body.game-lost { 34 | background: #ffeae6; 35 | } 36 | 37 | .container { 38 | max-width: 40rem; 39 | margin: auto; 40 | } 41 | 42 | header { 43 | margin-bottom: 2rem; 44 | border-bottom: 1px solid #d3d6da; 45 | display: flex; 46 | align-items: center; 47 | background: white; 48 | } 49 | 50 | .header-left, menu { 51 | width: 150px; 52 | } 53 | 54 | menu button { 55 | font-size: 2rem; 56 | padding: 1rem; 57 | } 58 | 59 | h1 { 60 | flex: 1 1 0; 61 | padding: 1rem 0; 62 | text-align: center; 63 | letter-spacing: .2rem; 64 | font-family: "Courier New", monospace; 65 | } 66 | 67 | h2 { 68 | margin-bottom: 1rem; 69 | } 70 | 71 | ul, h3 { 72 | margin-bottom: 1rem; 73 | } 74 | ul { 75 | padding-left: 1rem; 76 | } 77 | h3 { 78 | font-weight: normal; 79 | } 80 | p { 81 | margin-bottom: .3rem; 82 | } 83 | li { 84 | margin-bottom: 4px; 85 | } 86 | p.gap-under{ 87 | margin-bottom: 1rem; 88 | } 89 | .gap-above { 90 | margin-top: 2rem; 91 | } 92 | .credits { 93 | margin-top: 4rem; 94 | font-style: italic; 95 | } 96 | .small-gap-above { 97 | margin-top: .5rem; 98 | } 99 | a.has-title, .weak-link { 100 | color: black; 101 | text-decoration-line: underline; 102 | text-decoration-color: silver; 103 | } 104 | 105 | .scoreboard { 106 | padding: .7rem; 107 | margin-bottom: 1rem; 108 | text-align: center; 109 | border-bottom: 1px solid #d3d6da; 110 | 111 | display: flex; 112 | align-items: flex-end; 113 | } 114 | 115 | .scoreboard > .spacer { 116 | width: 7rem; 117 | } 118 | 119 | .see-stats { 120 | text-align: center; 121 | } 122 | .link { 123 | color: #006eff; 124 | text-decoration: underline; 125 | cursor: pointer; 126 | } 127 | .info-text { 128 | display: none; 129 | } 130 | 131 | #clue, .info-text { 132 | font-size: larger; 133 | flex: 1 1 0; 134 | } 135 | #clue > span { 136 | font-weight: bold; 137 | letter-spacing: .1rem; 138 | } 139 | 140 | #bottom { 141 | display: grid; 142 | grid-template-columns: repeat(3, 1fr); 143 | gap: 1rem; 144 | } 145 | .log-list { 146 | text-align: left; 147 | } 148 | .log-list, .card-list { 149 | list-style-type: none; 150 | margin: 0; 151 | padding: 0; 152 | } 153 | .card-list { 154 | display: flex; 155 | gap: .3rem; 156 | } 157 | .card-list { 158 | margin-bottom: .3rem; 159 | } 160 | .small-card { 161 | display: flex; 162 | flex-direction: column; 163 | justify-content: center; 164 | align-items: center; 165 | padding: 6px; 166 | width: 7rem; 167 | font-size: 80%; 168 | height: 3rem; 169 | text-align: center; 170 | background-color: var(--card-color); 171 | border-radius: 4px; 172 | } 173 | .small-card span { 174 | background: transparent !important; 175 | font-size: 80%; 176 | opacity: .7; 177 | margin-top: 1px; 178 | } 179 | 180 | 181 | #turn { 182 | font-weight: bold; 183 | } 184 | 185 | .board { 186 | display: grid; 187 | grid-template-columns: repeat(4, 1fr); /* Overwritten from javascript */ 188 | gap: 1rem; 189 | margin-bottom: 1rem; 190 | } 191 | 192 | .card { 193 | padding: 3rem 1rem 1rem 1rem; 194 | background-color: var(--card-color); 195 | border-radius: 5px; 196 | font-size: larger; 197 | font-weight: 500; 198 | cursor: pointer; 199 | overflow: hidden; 200 | 201 | display: flex; 202 | align-items: stretch; 203 | } 204 | .card > span { 205 | padding: .5rem; 206 | letter-spacing: .1rem; 207 | background: var(--card-light); 208 | 209 | flex: 1 1 0; 210 | display: flex; 211 | align-items: center; 212 | justify-content: space-evenly; 213 | } 214 | .bad span { 215 | background: var(--bad-light); 216 | } 217 | .good span { 218 | background: var(--good-light); 219 | } 220 | 221 | body.round-over .card { 222 | cursor: auto; 223 | } 224 | 225 | .good { 226 | background: var(--good-color); 227 | } 228 | 229 | .bad { 230 | background: var(--bad-color); 231 | color: white; 232 | } 233 | 234 | 235 | button { 236 | background: transparent; 237 | border: none; 238 | cursor: pointer; 239 | } 240 | 241 | .floating-circle { 242 | display: flex; 243 | gap: .5rem; 244 | justify-content: center; 245 | align-items: center; 246 | position: absolute; 247 | top: -2rem; 248 | right: -1rem; 249 | height: 5rem; 250 | padding: 1.5rem 2rem; 251 | border-radius: 2.5rem; 252 | background-color: black; 253 | color: white; 254 | box-shadow: 0 2px 5px rgba(0,0,0,0.3); 255 | text-align: center; 256 | cursor: pointer; 257 | z-index: 1000; /* Ensures it floats above other elements */ 258 | transition: transform 0.3s ease; /* Smooth transition for hover effect */ 259 | font-size: 105%; 260 | } 261 | .floating-circle i { 262 | font-size: 200%; 263 | } 264 | 265 | .floating-circle:hover { 266 | transform: scale(1.1); /* Scales the button on hover */ 267 | background: #333; 268 | } 269 | 270 | #bottom { 271 | position: relative; 272 | } 273 | #remainingClues { 274 | font-size: 110%; 275 | padding-right: 1rem; 276 | } 277 | 278 | body:not(.round-over) #endTurn, body.game-over #endTurn { 279 | visibility: hidden; 280 | } 281 | body.round-over #remainingClues, body.game-over #remainingClues { 282 | visibility: hidden; 283 | } 284 | 285 | body.round-over .board { 286 | opacity: .9; 287 | } 288 | body.game-over .info-text { 289 | display: block; 290 | } 291 | body.game-over #clue { 292 | display: none; 293 | } 294 | body:not(.game-over) .div-after-game { 295 | display: none; 296 | } 297 | .div-during-game { 298 | /*margin-top: 1rem*/ 299 | } 300 | body.game-over .div-during-game { 301 | display: none; 302 | } 303 | code { 304 | font-family: "Courier New", monospace; 305 | font-weight: bold; 306 | } 307 | body.today .not-today { 308 | display: none; 309 | } 310 | body:not(.today) .is-today { 311 | display: none; 312 | } 313 | 314 | @media only screen and (max-width: 600px) { 315 | .board { 316 | gap: .2rem; 317 | } 318 | button { 319 | font-size:100%; 320 | } 321 | #bottom { 322 | grid-template-columns: repeat(2, 1fr); 323 | } 324 | .spacer1 { 325 | display: none; 326 | } 327 | .scoreboard > .spacer { 328 | width: 6rem; 329 | } 330 | #hint { 331 | margin: 0 .2rem; 332 | } 333 | .header-left, menu { 334 | width: auto; 335 | } 336 | .card { 337 | padding: 1.5rem .5rem .5rem .5rem; 338 | } 339 | .spacer-left { 340 | display: none; 341 | } 342 | #clue { 343 | text-align: left; 344 | } 345 | .floating-circle { 346 | right: 1rem; 347 | } 348 | } 349 | 350 | /********************************************************************************/ 351 | /* Modal overlay */ 352 | /********************************************************************************/ 353 | 354 | /* Modal container */ 355 | .modal { 356 | display: none; /* Hidden by default */ 357 | position: fixed; /* Stay in place */ 358 | z-index: 1; /* Sit on top */ 359 | left: 0; 360 | top: 0; 361 | width: 100%; /* Full width */ 362 | height: 100%; /* Full height */ 363 | overflow: auto; /* Enable scroll if needed */ 364 | background-color: rgba(255,255,255,.8); /* Black w/ opacity */ 365 | } 366 | 367 | /* Modal content */ 368 | .modal-content { 369 | background-color: #fefefe; 370 | margin: 15% auto; 371 | padding: 30px 20px; 372 | border: 1px solid #c0c0c0; 373 | border-radius: 5px; 374 | width: 600px; 375 | box-shadow: 0 1px 12px 5px rgba(0,0,0,0.2); 376 | position: relative; 377 | } 378 | 379 | /* The Close Button */ 380 | .close { 381 | color: #aaa; 382 | float: right; 383 | font-size: 28px; 384 | font-weight: bold; 385 | position: absolute; 386 | top: 16px; 387 | right: 16px; 388 | } 389 | 390 | .close:hover, 391 | .close:focus { 392 | color: black; 393 | text-decoration: none; 394 | cursor: pointer; 395 | } 396 | 397 | @media only screen and (max-width: 600px) { 398 | .modal-content { 399 | width: 100%; 400 | } 401 | menu button { 402 | padding: .25rem; 403 | } 404 | } 405 | 406 | .statsItem { 407 | text-align: center; 408 | margin: 10px; 409 | display: flex; 410 | flex-direction: column; 411 | } 412 | 413 | .statsContainer { 414 | display: flex; 415 | flex-direction: row; 416 | } 417 | 418 | .big-number { 419 | font-size: 300%; 420 | } 421 | .number-label { 422 | font-size: 60%; 423 | width: 3rem; 424 | } 425 | 426 | #guessDistribution { 427 | display: flex; 428 | flex-direction: column; 429 | align-items: flex-start; 430 | } 431 | 432 | .guessBar { 433 | width: 0%; /* Initial width, will be set by JavaScript */ 434 | height: 20px; 435 | background-color: var(--good-color); 436 | text-align: right; 437 | padding-right: 5px; 438 | color: white; 439 | 440 | display: flex; 441 | align-items: center; 442 | justify-content: flex-end; 443 | } 444 | .guessBar-container { 445 | display: flex; 446 | width: 70%; 447 | margin: 2px 0; 448 | font-size: 80%; 449 | align-items: center; 450 | } 451 | .row-label { 452 | width: 1rem; 453 | } 454 | 455 | .share-button { 456 | display: flex; 457 | gap: .5rem; 458 | justify-content: center; 459 | align-items: center; 460 | 461 | margin:auto; 462 | 463 | height: 3rem; 464 | border-radius: 1.5rem; 465 | 466 | padding: 0 2rem; 467 | background-color: #5fab1b; 468 | color: white; 469 | 470 | cursor: pointer; 471 | font-size: 105%; 472 | } 473 | 474 | .modal-message { 475 | background: black; 476 | padding: 1rem; 477 | border-radius: 5px; 478 | color: white; 479 | 480 | position: absolute; 481 | top: 0; 482 | left: 50%; 483 | transform: translate(-50%, -110%); 484 | } 485 | 486 | -------------------------------------------------------------------------------- /wordlist: -------------------------------------------------------------------------------- 1 | AFRICA 2 | AGENT 3 | AIR 4 | ALIEN 5 | ALPS 6 | AMAZON 7 | AMBULANCE 8 | AMERICA 9 | ANGEL 10 | ANTARCTICA 11 | APPLE 12 | ARM 13 | ATLANTIS 14 | AUSTRALIA 15 | AZTEC 16 | BACK 17 | BALL 18 | BAND 19 | BANK 20 | BAR 21 | BARK 22 | BAT 23 | BATTERY 24 | BEACH 25 | BEAR 26 | BEAT 27 | BED 28 | BEIJING 29 | BELL 30 | BELT 31 | BERLIN 32 | BERMUDA 33 | BERRY 34 | BILL 35 | BLOCK 36 | BOARD 37 | BOLT 38 | BOMB 39 | BOND 40 | BOOM 41 | BOOT 42 | BOTTLE 43 | BOW 44 | BOX 45 | BRIDGE 46 | BRUSH 47 | BUCK 48 | BUFFALO 49 | BUG 50 | BUGLE 51 | BUTTON 52 | CALF 53 | CANADA 54 | CAP 55 | CAPITAL 56 | CAR 57 | CARD 58 | CARROT 59 | CASINO 60 | CAST 61 | CAT 62 | CELL 63 | CENTAUR 64 | CENTER 65 | CHAIR 66 | CHANGE 67 | CHARGE 68 | CHECK 69 | CHEST 70 | CHICK 71 | CHINA 72 | CHOCOLATE 73 | CHURCH 74 | CIRCLE 75 | CLIFF 76 | CLOAK 77 | CLUB 78 | CODE 79 | COLD 80 | COMIC 81 | COMPOUND 82 | CONCERT 83 | CONDUCTOR 84 | CONTRACT 85 | COOK 86 | COPPER 87 | COTTON 88 | COURT 89 | COVER 90 | CRANE 91 | CRASH 92 | CRICKET 93 | CROSS 94 | CROWN 95 | CYCLE 96 | CZECH 97 | DANCE 98 | DATE 99 | DAY 100 | DEATH 101 | DECK 102 | DEGREE 103 | DIAMOND 104 | DICE 105 | DINOSAUR 106 | DISEASE 107 | DOCTOR 108 | DOG 109 | DRAFT 110 | DRAGON 111 | DRESS 112 | DRILL 113 | DROP 114 | DUCK 115 | DWARF 116 | EAGLE 117 | EGYPT 118 | EMBASSY 119 | ENGINE 120 | ENGLAND 121 | EUROPE 122 | EYE 123 | FACE 124 | FAIR 125 | FALL 126 | FAN 127 | FENCE 128 | FIELD 129 | FIGHTER 130 | FIGURE 131 | FILE 132 | FILM 133 | FIRE 134 | FISH 135 | FLUTE 136 | FLY 137 | FOOT 138 | FORCE 139 | FOREST 140 | FORK 141 | FRANCE 142 | GAME 143 | GAS 144 | GENIUS 145 | GERMANY 146 | GHOST 147 | GIANT 148 | GLASS 149 | GLOVE 150 | GOLD 151 | GRACE 152 | GRASS 153 | GREECE 154 | GREEN 155 | GROUND 156 | HAM 157 | HAND 158 | HAWK 159 | HEAD 160 | HEART 161 | HELICOPTER 162 | HIMALAYAS 163 | HOLE 164 | HOLLYWOOD 165 | HONEY 166 | HOOD 167 | HOOK 168 | HORN 169 | HORSE 170 | HORSESHOE 171 | HOSPITAL 172 | HOTEL 173 | ICE 174 | INDIA 175 | IRON 176 | IVORY 177 | JACK 178 | JAM 179 | JET 180 | JUPITER 181 | KANGAROO 182 | KETCHUP 183 | KEY 184 | KID 185 | KING 186 | KIWI 187 | KNIFE 188 | KNIGHT 189 | LAB 190 | LAP 191 | LASER 192 | LAWYER 193 | LEAD 194 | LEMON 195 | LEPRECHAUN 196 | LIFE 197 | LIGHT 198 | LIMOUSINE 199 | LINE 200 | LINK 201 | LION 202 | LITTER 203 | LOCK 204 | LOG 205 | LONDON 206 | LUCK 207 | MAIL 208 | MAMMOTH 209 | MAPLE 210 | MARBLE 211 | MARCH 212 | MASS 213 | MATCH 214 | MERCURY 215 | MEXICO 216 | MICROSCOPE 217 | MILLIONAIRE 218 | MINE 219 | MINT 220 | MISSILE 221 | MODEL 222 | MOLE 223 | MOON 224 | MOSCOW 225 | MOUNT 226 | MOUSE 227 | MOUTH 228 | MUG 229 | NAIL 230 | NEEDLE 231 | NET 232 | NIGHT 233 | NINJA 234 | NOTE 235 | NOVEL 236 | NURSE 237 | NUT 238 | OCTOPUS 239 | OIL 240 | OLIVE 241 | OLYMPUS 242 | OPERA 243 | ORANGE 244 | ORGAN 245 | PALM 246 | PAN 247 | PANTS 248 | PAPER 249 | PARACHUTE 250 | PARK 251 | PART 252 | PASS 253 | PASTE 254 | PENGUIN 255 | PHOENIX 256 | PIANO 257 | PIE 258 | PILOT 259 | PIN 260 | PIPE 261 | PIRATE 262 | PISTOL 263 | PIT 264 | PITCH 265 | PLANE 266 | PLASTIC 267 | PLATE 268 | PLATYPUS 269 | PLAY 270 | PLOT 271 | POINT 272 | POISON 273 | POLE 274 | POLICE 275 | POOL 276 | PORT 277 | POST 278 | POUND 279 | PRESS 280 | PRINCESS 281 | PUMPKIN 282 | PUPIL 283 | PYRAMID 284 | QUEEN 285 | RABBIT 286 | RACKET 287 | RAY 288 | REVOLUTION 289 | RING 290 | ROBIN 291 | ROBOT 292 | ROCK 293 | ROME 294 | ROOT 295 | ROSE 296 | ROULETTE 297 | ROUND 298 | ROW 299 | RULER 300 | SATELLITE 301 | SATURN 302 | SCALE 303 | SCHOOL 304 | SCIENTIST 305 | SCORPION 306 | SCREEN 307 | SCUBA 308 | SEAL 309 | SERVER 310 | SHADOW 311 | SHAKESPEARE 312 | SHARK 313 | SHIP 314 | SHOE 315 | SHOP 316 | SHOT 317 | SINK 318 | SKYSCRAPER 319 | SLIP 320 | SLUG 321 | SMUGGLER 322 | SNOW 323 | SNOWMAN 324 | SOCK 325 | SOLDIER 326 | SOUL 327 | SOUND 328 | SPACE 329 | SPELL 330 | SPIDER 331 | SPIKE 332 | SPINE 333 | SPOT 334 | SPRING 335 | SPY 336 | SQUARE 337 | STADIUM 338 | STAFF 339 | STAR 340 | STATE 341 | STICK 342 | STOCK 343 | STRAW 344 | STREAM 345 | STRIKE 346 | STRING 347 | SUB 348 | SUIT 349 | SUPERHERO 350 | SWING 351 | SWITCH 352 | TABLE 353 | TABLET 354 | TAG 355 | TAIL 356 | TAP 357 | TEACHER 358 | TELESCOPE 359 | TEMPLE 360 | THEATER 361 | THIEF 362 | THUMB 363 | TICK 364 | TIE 365 | TIME 366 | TOKYO 367 | TOOTH 368 | TORCH 369 | TOWER 370 | TRACK 371 | TRAIN 372 | TRIANGLE 373 | TRIP 374 | TRUNK 375 | TUBE 376 | TURKEY 377 | UNDERTAKER 378 | UNICORN 379 | VACUUM 380 | VAN 381 | VET 382 | WAKE 383 | WALL 384 | WAR 385 | WASHER 386 | WASHINGTON 387 | WATCH 388 | WATER 389 | WAVE 390 | WEB 391 | WELL 392 | WHALE 393 | WHIP 394 | WIND 395 | WITCH 396 | WORM 397 | YARD 398 | --------------------------------------------------------------------------------