├── .gitignore ├── LICENSE ├── README.md ├── pyms ├── __init__.py ├── __main__.py ├── constants.py ├── gui.py └── recorder.py ├── pymsweeper.pyw └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pms 3 | *.log 4 | dist/* 5 | build/* -------------------------------------------------------------------------------- /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 | # `pymsweeper` 2 | > Oh yeah? I'm gonna build my own Minesweeper, with Blackjack, and hookers! 3 | > In fact, forget the hookers! 4 | 5 | It's Minesweeper... with a dash of blackjack... running on native Python `tkinter` 6 | 7 | # Requirements 8 | Just vanilla Python 3.5+ should do (due to type hinting) 9 | (Tested on Python 3.6 and 3.7) 10 | 11 | # How to use (3 alternatives) 12 | 1. Install `pyms` as a package (`pip install .` on package root) and run (`import pyms; pyms.run()`), or... 13 | 2. Run as module (`python -m pyms`), or... 14 | 3. Run `pymsweeper.pyw` as a script (`python pymsweeper.pyw`) 15 | 16 | # Instruction 17 | (Note: In the GUI, *IEDs* == *Mines*) 18 | 19 | There are two main modes, each comes with three sub levels: 20 | 1. Normal modes which mimic typical minesweeper 21 | 2. Blackjack modes which assigns a card value (like blackjack) to each mine 22 | 23 | ## Normal mode 24 | Just like typical minesweeper: 25 | 26 | 1. Left Click (Mouse1) to any cell, 27 | 2. Right Click (Mouse3) (or keyboard 1) to flag cell, 28 | 3. Left + Right Click(Mouse1 + Mouse3) to reveal adjacent 8 cells if the correct flagged. 29 | 30 | Option: 31 | 1. **Use Seed**: If you want to re/play a particular field, use the seed generator under option. 32 | - Highscores shows the seed number from your best attempts, so that you may challenge yourself again. 33 | - If the current field is generated from a seed, highscore will not be recorded. 34 | 35 | ## Blackjack mode 36 | Changes from Normal mode: 37 | 38 | 1. Mid Click mouse wheel (Mouse2) will allow users to confirm if the flagged value is correct (Read on for more info). 39 | 2. Right click to cycle through flags of `0`-`10`. 40 | 3. Keyboard bindings: The 3x4 area of 123QWEASDZXC are mapped accordingly to the card values, i.e.: 41 | 42 | 1 = `1`, 2 = `2`, 3 = `3` 43 | Q = `4`, W = `5`, E = `6` 44 | A = `7`, S = `8`, D = `9` 45 | Z = X = C = `10` 46 | 47 | Numpads and numbers are mapped as well, with 4 = `4`, 5 = `5`... and 0 = `10`. 48 | 49 | 4. Each mine is now assigned a value from `1-10`, much like cards in blackjack. 50 | 5. The clues shown are the `sum` of the card values in adjacent cells. 51 | 52 | For example, provided `□` represents empty cells, and `#`s represent card values: 53 | 54 | □ a 2 55 | c b 10 56 | □ 3 □ 57 | 58 | - clue `a` will show as `12` 59 | - clue `b` will show as `15` 60 | - clue `c` will show as `3` 61 | 62 | 6. Allow *Hits* (revealing of a valued mine) of up to `21` total points, depending on the *Hits* option selected: 63 | - **Disallow hits**: revealing *any* mine will be game over, much like normal mode. 64 | - **Allow Hits on guesses only**: use mid-click (Mouse2) on unopened cells to guess if the unflagged/flagged value is correct. 65 | - If the cell is flagged and the value matches, the guess is safe (marked green) 66 | - If the cell is flagged flag value doesn't match: 67 | - If it is a mine, it counts as a hit (follows any hits condition, see next section). 68 | - If it is not a mine, immediate game over. 69 | - If the cell is unflagged and it is not a mine, the guess is safe (marked green). 70 | - If the cell is unflagged and it is a mine, it counts as ahit (follows any hits condition, see next section). 71 | - **Allow Hits on any clicks**: revealing any mine will count as a hit. If mid-click was used, the above logic follows. 72 | - When the total hits accrue up to `17` points, the smiley face will frown as a warning. 73 | - When the total hits exceed `21` points, the game is immediately over. 74 | 75 | The more restrictive the mode (less help), with less guesses and less hits, the better the highscore. 76 | 77 | 7. Includes two helpful hint system (which can be disabled for higher scores): 78 | - **Mouseover Hints**: Calculate the flagged values on the hovered cell to show remaining flags required to match total. 79 | - **Flags Tracker**: Track how many flags have been used, and which values have been hit or guessed. 80 | 81 | # TODOs (in no particular order) 82 | 1. Add comments... 83 | 2. Clean up testing artifacts. 84 | 3. Think of an alternative for combining unicode as it doesn't show nicely on Linux and Win7. Windows 10 is fine. 85 | 86 | ## Cleared TODOs: 87 | 1. Identify false flags 88 | 2. First click no longer explodes 89 | 3. Some UI enhancement to update the visual 90 | 4. Blackjack-ish mode! 91 | 5. Added number hints with mouse over 92 | 6. Added number hints for flags 93 | 7. Options to disable hints 94 | 8. Confirm numbered flags to lock (will fail if flagged value not match) 95 | 9. Added handling for updating hinter with locked/hit numbers as well 96 | 10. Changed f-strings to `format` to support lower versions. 97 | 11. Added seeding - possibility to use seed to generate field. 98 | 12. Separated "hits" option to three - Disallow hits, allow hits on guesses, allow any hits. 99 | 13. Highscores, finally! 100 | 14. Added more symbols for association. 101 | 15. Highscore handling for corrupted loads. 102 | 16. Added save handling for mode and options. 103 | 17. Changed saving file format. 104 | 18. Changed main run script to consoleless mode. 105 | 19. Added basic package structure to support install and running as module. 106 | 20. Added UI hints (change background colour) when # of flags don't match. 107 | 21. Shuffled things around to allow for unflagged guesses. If the cell is not flagged and user took a guess, they'll just take the hit. 108 | 22. Added differentiation of colours between guesses and cleared cells on endgame. 109 | 23. Modified "`Use Seed...`" option to also display current and previous seed for retries. 110 | 24. If highscore is disabled due to seeding, the main field will be surrounded with a blue hue. 111 | 112 | ## Wishlist (ranked by preference) 113 | 1. Perform more testing on ranking to see if weight assigned is fair. 114 | 2. Add help popup to explain bindings, game modes, etc. 115 | 3. Custom mode to select grid size and rate/amount. 116 | 4. Balancing on number mode (more tests...) 117 | 5. UI tests to see how fonts/etc behave on different systems. 118 | 6. UI enhancements, e.g. image instead of text, alignments, etc. 119 | 7. Polish the package good enough to feel good about publishing on PyPI. 120 | 121 | # Fixes: 122 | 1. Fixed a potential issue if first click is flagged it would still trigger a `set_IEDs`. 123 | 2. Fixed an issue with `set_IEDs` being triggered more than once which interferes with seeds. 124 | 3. Fixed highscore ranking as the `sort_key` was sorting it in reverse. 125 | 4. Removed OS dependant colouring name. 126 | 5. Renamed to be unique on PyPI if I finally feel good enough to publish this... 127 | 6. Changed the structure to re-use the same `Field` object instead of creating a new instance each time. -------------------------------------------------------------------------------- /pyms/__init__.py: -------------------------------------------------------------------------------- 1 | from .gui import run 2 | -------------------------------------------------------------------------------- /pyms/__main__.py: -------------------------------------------------------------------------------- 1 | ''' Allows pyms to run as a module ''' 2 | 3 | if __name__ == '__main__': 4 | print('Running pyms as module') 5 | from . import gui 6 | gui.run() 7 | -------------------------------------------------------------------------------- /pyms/constants.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | 3 | # Mouse state constants 4 | MOUSE_LEFT = 2 ** 8 5 | MOUSE_MID = 2 ** 9 6 | MOUSE_RIGHT = 2 ** 10 7 | 8 | # Status constants 9 | STATUS = namedtuple('STATUS', 'icon fg bg font') 10 | STATUS_OKAY = STATUS('☺', 'black', 'gold', ('tkDefaultFont', 18, 'bold')) 11 | STATUS_WOAH = STATUS('☹', 'black', 'orange', ('tkDefaultFont', 24, 'bold')) 12 | STATUS_BOOM = STATUS('☠', 'white', 'red3', ('tkDefaultFont', 18, 'bold')) 13 | STATUS_YEAH = STATUS('✌', 'white', 'limegreen', ('tkDefaultFont', 18, 'bold')) 14 | 15 | # Mode constants 16 | MODE_CONFIG = namedtuple('MODE_CONFIG', 'name x y rate amount special') 17 | MODES = { 18 | 0: MODE_CONFIG('Fresh', 8, 8, None, 10, False), 19 | 1: MODE_CONFIG('Skilled', 16, 16, None, 40, False), 20 | 2: MODE_CONFIG('Pro', 30, 16, None, 99, False), 21 | 3: MODE_CONFIG('Half Deck', 12, 12, None, 52 // 2, True), 22 | 4: MODE_CONFIG('Full Deck', 16, 16, None, 52, True), 23 | 5: MODE_CONFIG('Double Deck', 28, 16, None, 52 * 2, True) 24 | } 25 | 26 | # Circled Number constants 27 | 28 | # CIRCLED_NUMBERS = {i + 1: chr(0x2780 + i) for i in range(10)} 29 | CIRCLED_NUMBERS = {i + 1: chr(0x2460 + i) for i in range(10)} 30 | 31 | # NEG_CIRCLED_NUMBERS = {i + 1: chr(0x2776 + i) for i in range(10)} 32 | NEG_CIRCLED_NUMBERS = {i + 1: chr(0x278A + i) for i in range(10)} 33 | 34 | # Numbered clues helper config 35 | TRACKER_CONFIG = namedtuple('TRACKER_CONFIG', 'max_check over_state tracked_num flag_state') 36 | 37 | # Mouse over hint config 38 | HINT = namedtuple('HINT', 'frame label counter') 39 | 40 | # GUI Options 41 | OPTIONS = namedtuple('OPTIONS', 'mode sound mouseover tracker allow_hits') 42 | 43 | # Record data to support record class (follows order to be shown in highscore) 44 | RECORD = namedtuple('RECORD', 45 | ''' 46 | time_val 47 | seed 48 | time_str 49 | IED_guesses 50 | IED_hits 51 | IED_blew 52 | opt_mouseover 53 | opt_tracker 54 | opt_allow_hits 55 | ''' 56 | ) 57 | -------------------------------------------------------------------------------- /pyms/gui.py: -------------------------------------------------------------------------------- 1 | ''' Main core of GUI ''' 2 | import tkinter as tk 3 | from tkinter.messagebox import showinfo 4 | from tkinter.simpledialog import askinteger 5 | 6 | from sys import maxsize 7 | from time import time 8 | from random import Random, randrange 9 | from . import constants as c 10 | from . import recorder 11 | 12 | 13 | class MyIntVar(tk.IntVar): 14 | ''' Subclassing the IntVar to add convenience methods ''' 15 | def increase(self, num=1): 16 | self.change(num) 17 | 18 | def decrease(self, num=1): 19 | self.change(-num) 20 | 21 | def change(self, num): 22 | self.set(self.get() + num) 23 | 24 | class GUI(tk.Tk): 25 | ''' Main tkinter class that hosts window configs ''' 26 | # pylint: disable=too-many-instance-attributes 27 | # Suppressing the pylint warning as there are a bunch of frames to consider 28 | # and would be easier accessed by name instead of dict 29 | 30 | def __init__(self): 31 | super().__init__() 32 | self.title('Pysweeper') 33 | 34 | # generic image to force compound sizing on widgets 35 | self.empty_image = tk.PhotoImage(width=1, height=1) 36 | 37 | # Create record instance and load records and options 38 | self.record_keeper = recorder.RecordKeeper(self) 39 | 40 | opt_val = self.record_keeper.load() 41 | if not opt_val: 42 | # set default values if nothing to load 43 | opt_val = [3, 0, 1, 1, 1] 44 | 45 | # Set up tk variables and create menus and timer 46 | self.options = c.OPTIONS( 47 | tk.IntVar(name='Mode'), 48 | *(tk.BooleanVar(name=opt_name) for opt_name in ('Warning Sound', 'Σ Mouseover Hint', '⚑ Flags Tracker')), 49 | tk.IntVar(name='Hits Option') 50 | ) 51 | for _idx, _opt in enumerate(self.options): 52 | _opt.set(opt_val[_idx]) 53 | _opt.trace('w', lambda *_, idx=_idx: self.option_callback(idx)) 54 | self.create_menus() 55 | self.wm_protocol('WM_DELETE_WINDOW', self.exit) 56 | self.taco_bell(self.options.sound.get()) 57 | self.timer = Timer(self) 58 | # self.field = None 59 | self.field = Field(self) 60 | 61 | # Set up a blank label for default fg and bg global, to be more OS friendly 62 | _lbl = tk.Label(self, text='') 63 | global DEFAULT_FG 64 | global DEFAULT_BG 65 | DEFAULT_FG = _lbl.cget('fg') 66 | DEFAULT_BG = _lbl.cget('bg') 67 | _lbl.destroy() 68 | del _lbl 69 | 70 | # Set up the frames 71 | self.frm_main = tk.Frame(self, padx=2, pady=2) 72 | self.frm_helper = tk.Frame(self) 73 | self.hinter = HintBar(self, self.frm_helper) 74 | self.clueshelper = NumbHelper(self, self.frm_helper) 75 | 76 | # Build the main frames 77 | self.build_status_bar() 78 | self.frm_main.grid(row=1, column=0, sticky=tk.NSEW) 79 | self.frm_helper.grid(row=2, column=0, sticky=tk.NSEW) 80 | self.frm_helper.grid_columnconfigure(index=0, weight=1) 81 | self.frm_helper.bind('', GUI.widget_exposed) 82 | self.hinter.build() 83 | self.build_field(c.MODES.get(self.options.mode.get())) 84 | 85 | def taco_bell(self, state): 86 | ''' Toggle bell ''' 87 | self.bell = super().bell if state else lambda: None 88 | 89 | def check_allow_hits(self, state): 90 | ''' Toggle hits if using numbered mode ''' 91 | if state > 0 and self.options.mode.get() >= 3: 92 | self.frm_IEDs.grid_configure(columnspan=1) 93 | self.frm_blew.grid() 94 | else: 95 | self.frm_IEDs.grid_configure(columnspan=2) 96 | self.frm_blew.grid_remove() 97 | # Change the current threshold if field exists, bypassing if field isn't created yet 98 | try: 99 | self.field.allow_threshold(state) 100 | except AttributeError: 101 | # field doesn't exist yet 102 | pass 103 | 104 | def option_callback(self, opt_index: int): 105 | ''' Callback option to call functions based on tkVar triggered ''' 106 | opt_value = self.options[opt_index].get() 107 | parser = [ 108 | self.build_field, 109 | self.taco_bell, 110 | self.hinter.show, 111 | self.clueshelper.show, 112 | self.check_allow_hits 113 | ] 114 | try: 115 | # pylint: disable=protected-access 116 | self.field._cached_options[opt_index] = max(self.field._cached_options[opt_index], opt_value) 117 | except AttributeError: 118 | # _cached_options does not exist yet, don't need to do anything. 119 | pass 120 | parser[opt_index](opt_value) 121 | 122 | @staticmethod 123 | def widget_exposed(evt): 124 | ''' Hide helper frame if all the hinters are disabled ''' 125 | if not any(child.winfo_viewable() for child in evt.widget.children.values()): 126 | evt.widget.config(height=1) 127 | 128 | def create_menus(self): 129 | ''' Menu setups ''' 130 | # Creating menus... 131 | menubar = tk.Menu(self) 132 | norm_modes = tk.Menu(self, tearoff=0) 133 | numb_modes = tk.Menu(self, tearoff=0) 134 | 135 | # Setting the modes... 136 | for idx, mode in c.MODES.items(): 137 | mode_menu = numb_modes if mode.special else norm_modes 138 | mode_menu.add_radiobutton( 139 | label=mode.name, 140 | value=idx, 141 | variable=self.options.mode 142 | ) 143 | 144 | # Adding difficulty menu... 145 | diff_menu = tk.Menu(self, tearoff=0) 146 | diff_menu.add_cascade(label='☺ Normal', menu=norm_modes) 147 | # diff_menu.add_cascade(label='♠ Blackjack', menu=numb_modes) 148 | diff_menu.add_cascade(label='♠ ⃞ Blackjack', menu=numb_modes) 149 | 150 | # Adding option menu... 151 | self.options_menu = tk.Menu(self, tearoff=0) 152 | self.special_menu = tk.Menu(self, tearoff=0) 153 | hits_menu = tk.Menu(self.special_menu, tearoff=0) 154 | 155 | o = self.options 156 | self.options_menu.add_command(label='Retry/Use seed...', command=self.ask_for_seed) 157 | self.options_menu.add_checkbutton(label=o.sound._name, variable=o.sound) #pylint: disable=protected-access 158 | self.special_menu.add_checkbutton(label=o.mouseover._name, variable=o.mouseover) #pylint: disable=protected-access 159 | self.special_menu.add_checkbutton(label=o.tracker._name, variable=o.tracker) #pylint: disable=protected-access 160 | 161 | hits_menu.add_radiobutton(label='⛔ Disallow Hits', value=0, variable=o.allow_hits) 162 | hits_menu.add_radiobutton(label='☕ Allow Hits on guesses only', value=1, variable=o.allow_hits) 163 | hits_menu.add_radiobutton(label='♿ Allow Hits on any clicks', value=2, variable=o.allow_hits) 164 | 165 | self.special_menu.add_cascade(label='☄ Hits', menu=hits_menu) 166 | self.options_menu.add_cascade(label='♠ ⃞ Blackjack', menu=self.special_menu) 167 | 168 | # Compile the menus together... 169 | menubar.add_cascade(label='Modes', menu=diff_menu) 170 | menubar.add_cascade(label='Options', menu=self.options_menu) 171 | menubar.add_command(label='Highscores', command=lambda x=self.options.mode: self.record_keeper.show(x.get())) 172 | self.config(menu=menubar) 173 | 174 | def ask_for_seed(self): 175 | ''' Dialog window to request and validate seed from user ''' 176 | cur_seed = self.field.seed 177 | prev_seed = self.field.previous_seed 178 | default_seed = cur_seed if cur_seed else prev_seed if prev_seed else '' 179 | seed = askinteger( 180 | 'Generate from seed', 181 | '\n'.join(( 182 | 'Please enter the seed number you wish to use.\n', 183 | 'Previous seed: {prev}{default}'.format( 184 | prev=str(prev_seed), 185 | default=' <-' if default_seed == prev_seed else '' 186 | ), 187 | 'Current seed: {cur}{default}'.format( 188 | cur=str(cur_seed), 189 | default=' <-' if default_seed == cur_seed else '' 190 | ), 191 | '\nNote: Highscores will NOT be recorded!' 192 | )), 193 | minvalue=0, 194 | maxvalue=maxsize, 195 | parent=self, 196 | initialvalue=default_seed 197 | ) 198 | if seed: 199 | self.build_field(mode=self.options.mode.get(), seed=seed) 200 | 201 | def build_status_bar(self): 202 | ''' Build the timer, big button and counter ''' 203 | # Create all the widgets and frames... 204 | self.frm_status = tk.Frame(self) 205 | self.frm_timer = tk.LabelFrame(self.frm_status, text='Time:') 206 | self.lbl_timer = tk.Label(self.frm_timer, textvariable=self.timer.string) 207 | self.btn_main = tk.Button( 208 | self.frm_status, 209 | image=self.empty_image, 210 | command=lambda: self.build_field(c.MODES.get(self.options.mode.get())), 211 | width=32, 212 | height=32, 213 | compound='c', 214 | relief=tk.GROOVE 215 | ) 216 | self.update_status(c.STATUS_OKAY) 217 | self.frm_IEDs = tk.LabelFrame(self.frm_status, text='IEDs:') 218 | self.lbl_IEDs = tk.Label(self.frm_IEDs, text='0') 219 | self.frm_blew = tk.LabelFrame(self.frm_status, text='Hits:') 220 | self.lbl_blew = tk.Label(self.frm_blew, text='0') 221 | 222 | # Grid management for tk... ugh 223 | self.frm_status.grid(row=0, column=0, sticky=tk.NSEW) 224 | for idx, weight in enumerate((6, 2, 3, 3)): 225 | self.frm_status.columnconfigure(index=idx, weight=weight) 226 | self.frm_timer.grid(row=0, column=0, sticky=tk.NSEW) 227 | self.btn_main.grid(row=0, column=1, sticky=tk.NS) 228 | self.frm_IEDs.grid(row=0, column=2, sticky=tk.NSEW) 229 | self.frm_blew.grid(row=0, column=3, sticky=tk.NSEW) 230 | self.lbl_timer.pack() 231 | self.lbl_IEDs.pack() 232 | self.lbl_blew.pack() 233 | 234 | def build_field(self, mode: c.MODE_CONFIG, seed=None): 235 | ''' Build the field frame, stop the timer and update the counters ''' 236 | # Quick check if int is provided, convert to MODE_CONFIG. 237 | if isinstance(mode, int): 238 | mode = c.MODES.get(mode) 239 | 240 | # See if possible to seperate the special mode later.... 241 | if mode.special: 242 | self.clueshelper.build(mode.amount // 13) 243 | self.options_menu.entryconfig('♠ ⃞ Blackjack', state=tk.NORMAL) 244 | for opt_index in range(2, 4): # mouseover and tracker 245 | self.option_callback(opt_index) 246 | else: 247 | self.options_menu.entryconfig('♠ ⃞ Blackjack', state=tk.DISABLED) 248 | if self.hinter.exists: 249 | self.hinter.show(False) 250 | if self.clueshelper.exists: 251 | self.clueshelper.destroy() 252 | 253 | # See if allow_hit frame needs to be hidden or shown 254 | self.check_allow_hits(self.options.allow_hits.get()) 255 | 256 | # Actually starting the field 257 | # if not self.field is None: 258 | # self.field.destroy() 259 | # self.field = Field(self, mode, seed=seed) 260 | self.field.build(mode=mode, seed=seed) 261 | self.lbl_IEDs.config(textvariable=self.field.IED_current) 262 | self.lbl_blew.config(textvariable=self.field.IED_hit) 263 | self.update_status(c.STATUS_OKAY) 264 | self.timer.reset() 265 | 266 | def update_status(self, status:c.STATUS): 267 | ''' Update main happy face button ''' 268 | self.btn_main.config( 269 | text=status.icon, 270 | fg=status.fg, 271 | bg=status.bg, 272 | font=status.font 273 | ) 274 | 275 | def exit(self, save=True): 276 | if save: self.record_keeper.save() 277 | self.destroy() 278 | self.quit() 279 | 280 | def run(self): 281 | self.mainloop() 282 | 283 | class Timer: 284 | ''' 285 | Timer object to manage... the timer... 286 | Most of the methods are rather self explanatory 287 | ''' 288 | def __init__(self, parent): 289 | self.parent = parent 290 | self.string = tk.StringVar() 291 | self._job = None 292 | self.start_time = 0 293 | self.end_time = 0 294 | self.active = False 295 | self.reset() 296 | 297 | def stop_update(self): 298 | self.active = False 299 | if self._job is not None: 300 | self.parent.after_cancel(self._job) 301 | self._job = None 302 | 303 | def reset(self): 304 | self.stop_update() 305 | self.string.set('00:00:00') 306 | self.start_time = 0 307 | self.end_time = 0 308 | 309 | def start(self): 310 | self.start_time = time() 311 | self.active = True 312 | self._update() 313 | 314 | def _update(self): 315 | if self.active: 316 | self.to_string() 317 | self._job = self.parent.after(ms=1000, func=self._update) 318 | 319 | def to_string(self): 320 | ''' Format and set the string variable to the time ''' 321 | current = time() - self.start_time 322 | h, m, s = int(current // 3600), int(current % 3600 // 60), int(current % 60) 323 | self.string.set('{h:02}:{m:02}:{s:02}'.format(h=h, m=m, s=s)) 324 | 325 | def stop(self): 326 | self.end_time = time() - self.start_time 327 | self.to_string() 328 | self.stop_update() 329 | 330 | 331 | class Field: 332 | ''' The field that contains all the Map elements and handle the events ''' 333 | # pylint: disable=too-many-instance-attributes 334 | # Suppress pylint warning for now 335 | # Want to see if some attributes can be handled as classes 336 | 337 | def __init__(self, parent: GUI): 338 | self.parent = parent 339 | self.frame = None 340 | self.__used_seed = False 341 | self.seed = None 342 | self.previous_seed = None 343 | 344 | @property 345 | def used_seed(self): 346 | return self.__used_seed 347 | 348 | @used_seed.setter 349 | def used_seed(self, b_val): 350 | self.__used_seed = b_val 351 | if b_val: 352 | self.parent.frm_main.config(bg='light sky blue') 353 | # self.parent.frm_timer.config(bg='light sky blue') 354 | # self.parent.frm_IEDs.config(bg='light sky blue') 355 | # self.parent.frm_blew.config(bg='light sky blue') 356 | else: 357 | self.parent.frm_main.config(bg=DEFAULT_BG) 358 | # self.parent.frm_timer.config(bg=DEFAULT_BG) 359 | # self.parent.frm_IEDs.config(bg=DEFAULT_BG) 360 | # self.parent.frm_blew.config(bg=DEFAULT_BG) 361 | 362 | def allow_threshold(self, state=0): 363 | ''' Enable or disable hits threshold ''' 364 | self.IED_threshold = 21 if state > 0 else 0 365 | 366 | def build(self, mode: c.MODE_CONFIG = c.MODES.get(0), seed: int = None): 367 | ''' Build the frame and map elements ''' 368 | if not self.frame is None: 369 | self.frame.destroy() 370 | self.previous_seed = self.seed 371 | self.mode = mode 372 | self.frame = tk.Frame(master=self.parent.frm_main) 373 | self.is_over = False 374 | self.seed = seed 375 | self.used_seed = seed is not None 376 | 377 | # The original intent was to use rate to determine amount, 378 | # left here as a legacy, might be revisited 379 | if self.mode.amount: 380 | self.IED_count = self.mode.amount 381 | else: 382 | self.IED_count = int(self.mode.x * self.mode.y * self.mode.rate) 383 | self.IED_current = MyIntVar(value=self.IED_count) 384 | self.IED_guessed = 0 385 | self.IEDs = set() 386 | self._IEDs_are_set = False 387 | self.map_cleared = 0 388 | self.map = { 389 | (x, y): NumbedMapElem(self, (x, y)) if self.mode.special else MapElem(self, (x, y)) 390 | for x in range(self.mode.x) 391 | for y in range(self.mode.y) 392 | } 393 | if mode.special: 394 | self.allow_threshold(self.parent.options.allow_hits.get()) 395 | else: 396 | self.IED_threshold = 0 397 | self.IED_hit = MyIntVar(value=0) 398 | self.IED_blew = 0 399 | self.map_goal = self.mode.x * self.mode.y - self.IED_count 400 | for elem in self.map.values(): 401 | elem.build_surprise_box() 402 | self.frame.pack_propagate(False) 403 | self.frame.pack() 404 | 405 | def set_IEDs(self, current_coord: tuple = None): 406 | ''' Initial planting of IEDs on first click ''' 407 | # check if set_IEDs has already been called 408 | if not self._IEDs_are_set: 409 | # check if seed was provided, if not, generate a new seed 410 | if self.seed is None: 411 | self.seed = randrange(maxsize) 412 | rnd = Random(self.seed) 413 | # Randomize coord and add set if it's not the current location 414 | while len(self.IEDs) < self.IED_count: 415 | coord = (rnd.randrange(self.mode.x), rnd.randrange(self.mode.y)) 416 | 417 | # if seed was used, ignore validation of current coord 418 | if coord != current_coord or self.used_seed: 419 | self.IEDs.add(coord) 420 | 421 | # Use card values if Blackjack mode, else IEDs are assigned default value of 1 (True) 422 | if self.mode.special: 423 | cards = list(range(1, 10)) + [10] * 4 424 | cards = cards * (self.mode.amount // 13) 425 | rnd.shuffle(cards) 426 | for IED in sorted(self.IEDs): 427 | self.map.get(IED).is_IED = cards.pop() 428 | else: 429 | for IED in sorted(self.IEDs): 430 | self.map.get(IED).is_IED = 1 431 | 432 | self._IEDs_are_set = True 433 | self._cached_options = [opt.get() for opt in self.parent.options] 434 | # Start the timer once everything is set up 435 | self.parent.timer.start() 436 | 437 | def expose_IEDs(self, clear, show_false_flags=False): 438 | ''' Reveal unflagged IEDs and false flags when over ''' 439 | for IED in self.IEDs: 440 | self.map[IED].reveal(over_and_clear=clear) 441 | if show_false_flags: 442 | for elem in self.map.values(): 443 | if elem.flagged: 444 | elem.check_false_flag() 445 | 446 | def check_threshold(self, elem, guess_safe=None): 447 | ''' Check if threshold is exceeded ''' 448 | # # Added guessed argument to support mid-click guesses 449 | # if guessed: 450 | # self.IED_guessed += 1 451 | # else: 452 | # self.IED_current.decrease() 453 | if not guess_safe: 454 | self.IED_hit.increase(elem.is_IED) 455 | self.IED_blew += 1 456 | current_hit = self.IED_hit.get() 457 | if current_hit > self.IED_threshold or (self.parent.options.allow_hits.get() < 2 and guess_safe is None): 458 | self.bewm(elem) 459 | elif current_hit >= 17: 460 | self.parent.update_status(c.STATUS_WOAH) 461 | 462 | def bewm(self, last): 463 | ''' When the field blows up ''' 464 | self.parent.timer.stop() 465 | last.is_final() 466 | self.parent.update_status(c.STATUS_BOOM) 467 | self.is_over = True 468 | self.expose_IEDs(clear=False, show_false_flags=True) 469 | 470 | def check_clear(self, guessed=False): 471 | ''' Check for when the field is cleared ''' 472 | self.map_cleared += 1 473 | if guessed: 474 | self.IED_guessed += 1 475 | if self.map_cleared >= self.map_goal: 476 | self.parent.timer.stop() 477 | self.is_over = True 478 | self.parent.update_status(c.STATUS_YEAH) 479 | self.expose_IEDs(clear=True) 480 | congrats = 'You did it!\nTotal Time: {time}'.format(time=self.parent.timer.string.get()) 481 | if self.IED_threshold > 0: 482 | congrats += '\n\nYou took {n} guess{plural}.'.format(n=self.IED_guessed, plural='es' if self.IED_guessed > 1 else '') 483 | hit = self.IED_hit.get() 484 | if hit: 485 | congrats += '\n... And you hit {hit} point{plural}.\nAim for 0 next time!'.format(hit=hit, plural="s" if hit > 1 else "") 486 | else: 487 | congrats += '\nAnd you managed to remain clear without hitting any mines.\nCongrats!' 488 | if self.used_seed: 489 | congrats += '\n\n(Highscore not added as seed has been used)' 490 | else: 491 | self.parent.record_keeper.add_record( 492 | self.mode, 493 | c.RECORD( 494 | self.parent.timer.end_time, 495 | self.seed, 496 | self.parent.timer.string.get(), 497 | *( 498 | ( 499 | self.IED_guessed, 500 | self.IED_hit.get(), 501 | self.IED_blew, 502 | *self._cached_options[-3:] 503 | ) if self.mode.special else (0, ) * 6 504 | ) 505 | ) 506 | ) 507 | showinfo('Awesome!', congrats) 508 | 509 | def gradient_colour(main:int, increm=0x080808, n=8, darken=True, as_string=False) -> list: 510 | ''' 511 | Create a set of gradient colours based on the proided main colour, returns a list. 512 | 513 | main = starting RGB value 514 | increm = value to change the gradient shades by (default: 0x080808) 515 | n = number of total colours to return (default: 8) 516 | darken = if True, colours will decrease in value (darken), if False, the other direction (default: True) 517 | as_string = if True, returns as RBG string value "#FFFFFF", else, in integer (default: False) 518 | ''' 519 | if isinstance(main, str): 520 | try: 521 | main = int(main, 16) 522 | except ValueError: 523 | return [] 524 | if as_string: 525 | colours = ['#{:06x}'.format(main + (-i if darken else i) * increm) for i in range(n)] 526 | else: 527 | colours = [main + (-i if darken else i) * increm for i in range(n)] 528 | return colours 529 | 530 | def zip_gradient(colours: list, **kwargs): 531 | ''' 532 | Using gradient colour method, return a list of the gradient tuples transposed by the original list 533 | i.e. if a list of [a, b, c] was provided, returns: 534 | [ 535 | (a1, b1, c1), 536 | (a2, b2, c2), 537 | (a3, b3, c3), 538 | ... 539 | ] 540 | ''' 541 | kwargs = {kw: val for kw, val in kwargs.items() if kw in ('increm', 'n', 'darken', 'as_string')} 542 | grads = [gradient_colour(c, **kwargs) for c in colours] 543 | return list(zip(*grads)) 544 | 545 | class MapElem: 546 | ''' Map element base class to handle individual cells ''' 547 | 548 | # Colours associated with clue numbers 549 | clue_colours = { 550 | 1: 'blue', 551 | 2: 'forest green', 552 | 3: 'red2', 553 | 4: 'navy', 554 | 5: 'maroon', 555 | 6: 'cyan4', 556 | 7: 'purple', 557 | 8: 'seashell4', 558 | 9: 'goldenrod', 559 | 10: 'pink4' 560 | } 561 | def __init__(self, field: Field, coord): 562 | self.field = field 563 | self.coord = coord 564 | self.frame = tk.Frame(self.field.frame, width=24, height=24) 565 | self.frame.pack_propagate(False) 566 | self.is_IED = 0 567 | self.clue = 0 568 | self._flagged = 0 569 | self.revealed = False 570 | self.clueshelper = self.field.parent.clueshelper 571 | self.box = None 572 | self.lbl = None 573 | self._adjacents = None 574 | 575 | @property 576 | def flagged(self): 577 | ''' Returns whether the cell is flagged ''' 578 | return self._flagged 579 | 580 | @flagged.setter 581 | def flagged(self, num): 582 | ''' Flag cells and manage clue tracker, and IED count ''' 583 | # slightly ugly way to standardize the flag handling below instead of a bunch of if/else statements. 584 | for i, check in enumerate((self._flagged, num)): 585 | i = i * 2 - 1 586 | # so that 0 = -1, 1 = 1; i.e. num=0 will decrease flag and IED, num=1 will increase. 587 | if check != 0: 588 | self.clueshelper.change_flag(check, i) 589 | elif not self.revealed: 590 | self.field.IED_current.change(i) 591 | self._flagged = num 592 | 593 | def get_flag_config(self, num=None): 594 | ''' Config how the flag should display ''' 595 | # pylint: disable=unused-argument 596 | # The num argument is there to be consistent with the child class that relies on the same flag method. 597 | # Eventually will want to restructure this properly 598 | return {'text': '⚑'} 599 | 600 | def flag(self, num=None): 601 | ''' Handles updating of the concealer box visual ''' 602 | if num is None: 603 | num = 1 604 | if self.flagged == num: 605 | self.box.config(text=' ') 606 | self.flagged = 0 607 | else: 608 | self.box.config(**self.get_flag_config(num)) 609 | self.flagged = num 610 | 611 | def check_false_flag(self): 612 | ''' Check if box is false flagged ''' 613 | if not self.is_IED: 614 | self.box.config(**self.get_false_guess_config()) 615 | 616 | def get_false_guess_config(self): 617 | ''' config for false flag checker ''' 618 | return {'text': '❌', 'fg': 'white', 'bg': 'maroon'} 619 | 620 | def build_surprise_box(self): 621 | ''' Build the concealer button ''' 622 | self.box = Surprise(self) 623 | self.frame.grid(row=self.coord[1], column=self.coord[0], sticky=tk.NSEW) 624 | self.box.pack() 625 | return self.box 626 | 627 | @property 628 | def adjacents(self): 629 | ''' Set or initialize adjacent cell references ''' 630 | if self._adjacents is None: 631 | cx, cy = self.coord 632 | mapper = self.field.map 633 | self._adjacents = [mapper.get((rx, ry)) for rx in range(cx-1, cx+2) for ry in range(cy-1, cy+2) if mapper.get((rx, ry))] 634 | self._adjacents.remove(self) 635 | return self._adjacents 636 | 637 | def adjacent_IEDs(self): 638 | ''' Find adjacent IED totals ''' 639 | return 0 if self.is_IED else sum(adj.is_IED for adj in self.adjacents) 640 | 641 | def adjacent_flags(self): 642 | ''' Find adjacent Flag totals ''' 643 | return sum(adj.flagged + (adj.is_IED * int(adj.revealed)) for adj in self.adjacents) 644 | 645 | def get_IED_config(self, final=False): 646 | ''' Provide the config of how the IED is represented ''' 647 | config = { 648 | True: {'text': '✨', 'fg': 'red'}, 649 | False: {'text' : '☀'} 650 | }.get(final) 651 | config.update({'font' : ('tkDefaultFont', 12, 'bold')}) 652 | return config 653 | 654 | def is_final(self): 655 | ''' The last elem config before the field is blown ''' 656 | if self.is_IED: 657 | self.lbl.config(**self.get_IED_config(final=True)) 658 | else: 659 | self.lbl.config(**self.get_false_guess_config()) 660 | if self.clue: 661 | self.lbl.config(text=self.clue) 662 | 663 | def label_actual(self): 664 | ''' Set up the underlayer label ''' 665 | # This is separated so it's easier to manage the subclass 666 | if self.is_IED: 667 | actual = self.get_IED_config() 668 | else: 669 | actual = { 670 | 'text' : self.clue if self.clue else '', 671 | 'fg' : self.__class__.clue_colours.get(self.clue, DEFAULT_FG), 672 | 'font' : ('tkDefaultFont', 10, 'bold'), 673 | } 674 | 675 | lbl = tk.Label(master=self.frame, **actual) 676 | return lbl 677 | 678 | def create_actual(self): 679 | ''' Create the underlayer label ''' 680 | self.lbl = self.label_actual() 681 | if self.is_IED == 0: 682 | self.lbl.bind('', self.omni_click) 683 | self.lbl.bind('', self.omni_click) 684 | self.lbl.pack(fill=tk.BOTH, expand=True) 685 | 686 | def clicked(self, guess_safe=None): 687 | ''' Concealer box was clicked ''' 688 | go_ahead = self.reveal(guess_safe=guess_safe) 689 | if go_ahead: 690 | # Open adjacent cells if current is empty 691 | if self.clue == 0 and not self.is_IED: 692 | for adj in self.adjacents: 693 | adj.clicked() 694 | 695 | # Do the check regardless if guessed, safe or not. 696 | if self.is_IED: 697 | self.field.check_threshold(self, guess_safe=guess_safe) 698 | elif guess_safe is False: # and is not IED 699 | self.field.bewm(self) 700 | else: 701 | self.field.check_clear() 702 | 703 | def reveal(self, guess_safe=None, over_and_clear=None): 704 | ''' Reveal the block if not already revealed ''' 705 | # This go_ahead is needed to stop the recursion of adjacent clicking from happening 706 | # It removes the need to do the same check twice in both methods 707 | go_ahead = (not self.revealed and (self.flagged == 0 or guess_safe is not None)) 708 | if go_ahead: 709 | if self.field.map_cleared == 0: 710 | self.field.set_IEDs(self.coord) 711 | self.revealed = True 712 | if self.flagged: 713 | self.flagged = 0 714 | elif self.is_IED and over_and_clear is None: # and not flagged 715 | self.field.IED_current.decrease() 716 | self.clue = self.adjacent_IEDs() 717 | self.create_actual() 718 | self.box.pack_forget() 719 | 720 | # Check if it's guess_safe and in a winning condition to highlight mines 721 | if guess_safe: # or over_and_clear: 722 | self.lbl.config(bg='pale green') 723 | if over_and_clear: 724 | self.lbl.config(bg='lightblue') 725 | 726 | # If the game is over, skip updating the hinter. 727 | if self.is_IED and over_and_clear is None: 728 | self.clueshelper.guessed_flag(self.is_IED, guess_safe=guess_safe) 729 | 730 | # pass the condition back to self.clicked 731 | return go_ahead 732 | 733 | def omni_click(self, evt, ignore=False): 734 | ''' Main handler for clicking, branches off to sub methods... ''' 735 | if not self.field.is_over: 736 | # Make sure the cursor is within the same block, allow users to change their mind. 737 | w, h = evt.widget.winfo_geometry().replace('+', 'x').split('x')[:2] 738 | if evt.x in range(int(w)) and evt.y in range(int(h)): 739 | # Both buttons are pressed 740 | if (evt.num == 1 and evt.state & c.MOUSE_RIGHT) or (evt.num == 3 and evt.state & c.MOUSE_LEFT): 741 | self.both_release() 742 | elif evt.num == 1: 743 | self.left_release() 744 | # Mid click for special mode 745 | elif evt.num == 2: 746 | # if self.flagged: 747 | self.field.IED_guessed += 1 748 | self.clicked(guess_safe=self.flagged == self.is_IED) 749 | elif evt.num == 3 and not ignore: 750 | self.right_release() 751 | 752 | def left_release(self): 753 | ''' Remove the concealer ''' 754 | self.clicked() 755 | 756 | def right_release(self): 757 | ''' Flag the concealer ''' 758 | ### still trying to figure out if right release can be separated from right click for fast flagging 759 | # unheld = (evt.state & c.MOUSE_LEFT > 0) if evt else True 760 | # if not self.revealed and unheld: 761 | if not self.revealed: 762 | self.flag() 763 | 764 | def both_release(self): 765 | ''' Open adjacent blocks ''' 766 | if self.adjacent_flags() == self.clue and self.revealed: 767 | self.clicked() 768 | for adj in self.adjacents: 769 | adj.clicked() 770 | else: 771 | if self.lbl is not None: 772 | self._update_lbl_from_failed_reveal() 773 | self.field.parent.bell() 774 | 775 | def _update_lbl_from_failed_reveal(self, previous=None): 776 | ''' Flip the states of the current label ''' 777 | wrong_colour = 'gold' 778 | if previous is None: 779 | current = self.lbl.cget('bg') 780 | if not current == wrong_colour: 781 | # to handle multiple clicks; if already changed, don't set a new task. 782 | self.lbl.config(bg=wrong_colour) 783 | self.field.parent.after(ms=250, func=lambda: self._update_lbl_from_failed_reveal(current)) 784 | else: 785 | # return to the original colour. 786 | self.lbl.config(bg=previous) 787 | 788 | 789 | class NumbedMapElem(MapElem): 790 | ''' Subclassed Numbered Map element for Blackjack mode ''' 791 | # use original Clue colours for IED colours 792 | val_colours = MapElem.clue_colours 793 | # set new gradient for new clue colours as they can get up to 80 now. 794 | clue_colours = { 795 | i: clue_colour for i, clue_colour in enumerate( 796 | colour for c_set in zip_gradient( 797 | [ 798 | 0x804868, 799 | 0x5858C0, 800 | 0x58C058, 801 | 0xC05858, 802 | 0x58C0C0, 803 | 0xC0C058, 804 | 0xC058C0, 805 | 0xA06048, 806 | 0x6048A0, 807 | 0x48A060 808 | ], 809 | as_string=True 810 | ) for colour in c_set 811 | ) 812 | } 813 | 814 | def right_release(self): 815 | ''' overriden right_release to cycle through 0 - 10 with each click ''' 816 | if not self.revealed: 817 | self.flag((self.flagged + 1) % 11) 818 | 819 | def get_IED_config(self, final=False): 820 | config = { 821 | 'text' : c.NEG_CIRCLED_NUMBERS.get(self.is_IED), 822 | 'fg' : NumbedMapElem.val_colours.get(self.is_IED), 823 | 'font' : ('tkDefaultFont', 12) 824 | } 825 | if not self.field.is_over: 826 | config.update( 827 | { 828 | 'bg': 'gold', # 'maroon' 829 | 'relief': tk.SUNKEN 830 | } 831 | ) 832 | if final: 833 | config.update({'fg': 'white', 'bg': 'red3'}) 834 | return config 835 | 836 | def get_flag_config(self, num=None): 837 | return { 838 | 'text': c.CIRCLED_NUMBERS.get(num, ' '), 839 | 'fg': NumbedMapElem.val_colours.get(num), 840 | 'font': ('tkDefaultFont', 12) 841 | } 842 | 843 | def create_actual(self): 844 | super().create_actual() 845 | if self.is_IED == 0: 846 | self.lbl.bind('', lambda e: self.field.parent.hinter.update(self)) 847 | self.lbl.bind('', self.field.parent.hinter.reset) 848 | 849 | def build_surprise_box(self): 850 | self.box = NumbedSurprise(self) 851 | self.frame.grid(row=self.coord[1], column=self.coord[0], sticky=tk.NSEW) 852 | self.box.pack() 853 | return self.box 854 | 855 | def check_false_flag(self): 856 | if self.flagged != self.is_IED: 857 | self.box.config(**self.get_IED_config()) 858 | self.box.config(bg='orange') 859 | super().check_false_flag() 860 | 861 | 862 | class Surprise(tk.Button): 863 | ''' Concealer button object to handle bindings ''' 864 | 865 | def __init__(self, parent, *args, **kwargs): 866 | self.parent = parent 867 | super().__init__( 868 | master=self.parent.frame, 869 | text=' ', 870 | fg='orange red', 871 | image=self.parent.field.parent.empty_image, 872 | width=20, height=20, 873 | compound='c', 874 | relief=tk.GROOVE, 875 | *args, **kwargs 876 | ) 877 | self.flag = self.parent.flag 878 | 879 | self.bind('', self.parent.omni_click) 880 | self.bind('', self.parent.omni_click) 881 | self.bind('', lambda e: self.focus_set()) 882 | self.bind('', lambda e: self.parent.frame.focus_set()) 883 | self.set_other_bindings() 884 | 885 | def set_other_bindings(self): 886 | self.bind('1', lambda evt: self.flag(None)) 887 | 888 | 889 | class NumbedSurprise(Surprise): 890 | ''' Subclass concealer button to handle additional flagging ''' 891 | 892 | def set_other_bindings(self): 893 | self.bind('', self.parent.omni_click) 894 | 895 | # NUM binding 896 | for i, s in enumerate('1234567890', 1): 897 | if i > 10: i = 10 898 | self.bind(s, lambda e, x=i: self.flag(x)) 899 | 900 | # WASD binding 901 | for i, s in enumerate('qweasdzxc', 4): 902 | if i > 10: i = 10 903 | self.bind(s, lambda e, x=i: self.flag(x)) 904 | self.bind(s.upper(), lambda e, x=i: self.flag(x)) 905 | 906 | class HintBar: 907 | ''' Hint bar to help users calculate remaining flags ''' 908 | 909 | def __init__(self, gui: GUI, parent_frame): 910 | self.gui = gui 911 | self.frame = None 912 | self.parent_frame = parent_frame 913 | self.hints = None 914 | self.exists = False 915 | 916 | def build(self): 917 | ''' build the HintBar frame ''' 918 | if self.exists: 919 | self.destroy() 920 | self.frame = tk.Frame(master=self.parent_frame) 921 | self.hints = { 922 | k: self.create_inner_frame(k) 923 | for k in ('Total', 'Flags/Hits', 'Remaining') 924 | } 925 | for i, hint in enumerate(self.hints.values()): 926 | hint.frame.grid(row=0, column=i, sticky=tk.NSEW) 927 | hint.label.pack(fill=tk.BOTH, expand=True) 928 | for i in range(3): 929 | self.frame.columnconfigure(index=i, weight=1) 930 | self.exists = True 931 | 932 | def show(self, state): 933 | ''' Toggler to show or hide the frame ''' 934 | if state: 935 | self.frame.grid(row=0, column=0, sticky=tk.NSEW) 936 | else: 937 | self.frame.grid_remove() 938 | 939 | def create_inner_frame(self, ctype): 940 | def validate(hinter): 941 | if hinter.counter.get() < 0: 942 | hinter.label.config(bg='yellow') 943 | else: 944 | hinter.label.config(bg=DEFAULT_BG) 945 | frame = tk.LabelFrame(master=self.frame, text='{}:'.format(ctype)) 946 | counter = tk.IntVar() 947 | label = tk.Label(master=frame, textvariable=counter) 948 | hinter = c.HINT(frame, label, counter) 949 | counter.trace('w', lambda *args: validate(hinter)) 950 | return hinter 951 | 952 | def update(self, hinter: NumbedMapElem): 953 | if not self.gui.field.is_over: 954 | total = hinter.clue 955 | flags_hits = hinter.adjacent_flags() 956 | remaining = total - flags_hits 957 | self.hints['Total'].counter.set(total) 958 | self.hints['Flags/Hits'].counter.set(flags_hits) 959 | self.hints['Remaining'].counter.set(remaining) 960 | 961 | def reset(self, *args): 962 | ''' Reset hintbar to zeroes ''' 963 | #pylint: disable=unused-argument 964 | # The star arugment is to bypass the binding events. 965 | if not self.gui.field.is_over: 966 | for hint in self.hints.values(): 967 | hint.counter.set(0) 968 | 969 | def destroy(self): 970 | self.exists = False 971 | self.frame.destroy() 972 | 973 | class NumbTracker: 974 | def __init__(self, maximum): 975 | self.maximum = maximum 976 | self.blew_count = 0 977 | self.lock_count = 0 978 | self.flag_count = 0 979 | 980 | @property 981 | def total(self): 982 | return sum((self.flag_count, self.blew_count, self.lock_count)) 983 | 984 | @property 985 | def over(self): 986 | return self.total > self.maximum 987 | 988 | def change(self, change=1): 989 | self.flag_count = max(0, self.flag_count + change) 990 | 991 | def blew(self): 992 | self.blew_count += 1 993 | 994 | def lock(self): 995 | self.lock_count += 1 996 | 997 | class NumbHelper(tk.Frame): 998 | ''' Helper Frame object to help track flags ''' 999 | FLAG_ACTIVE = 'forestgreen' 1000 | FLAG_LOCK = 'dodger blue' 1001 | FLAG_BLEW = 'red2' 1002 | FLAG_OVER = 'gold' 1003 | FLAG_INACTIVE = 'LightCyan3' 1004 | def __init__(self, parent, parent_frame): 1005 | self.parent = parent 1006 | self.parent_frame = parent_frame 1007 | self.nrows = None 1008 | self.trackers = None 1009 | self.exists = False 1010 | self.tracker_configs = { 1011 | 1: c.TRACKER_CONFIG(1, NumbHelper.FLAG_OVER, 0, NumbHelper.FLAG_ACTIVE), 1012 | -1: c.TRACKER_CONFIG(0, DEFAULT_BG, 1, NumbHelper.FLAG_INACTIVE) 1013 | } 1014 | 1015 | def build(self, nrows): 1016 | if self.exists: 1017 | self.destroy() 1018 | self.nrows = nrows 1019 | self.trackers = { 1020 | i: NumbTracker(self.nrows * (4 if i >= 10 else 1)) 1021 | for i in range(1, 11) 1022 | } 1023 | super().__init__(master=self.parent_frame) 1024 | self.create_labels() 1025 | self.exists = True 1026 | 1027 | def show(self, state=True): 1028 | self.grid(row=1, column=0) if state else self.grid_remove() 1029 | 1030 | def create_labels(self): 1031 | self.lbls = { 1032 | # tuple key set up as (number, count=1, 2, 3, 4...) 1033 | (num, count + 1) : tk.Label( 1034 | master=self, 1035 | image=self.parent.empty_image, 1036 | text=c.NEG_CIRCLED_NUMBERS.get(num), 1037 | font=('tkDefaultFont', 12), 1038 | compound='c', 1039 | width=16, 1040 | height=12, 1041 | fg=NumbHelper.FLAG_INACTIVE 1042 | ) for num in range(1, 11) for count in range(self.nrows if num < 10 else self.nrows * 4) 1043 | } 1044 | for (num, count), lbl in self.lbls.items(): 1045 | num -= 1 1046 | count -= 1 1047 | if num < 9: # no longer checking 10 because of count -=1 1048 | lbl.grid(row=count, column=num) 1049 | else: 1050 | lbl.grid(row=count // 4, column=num + count % 4) 1051 | 1052 | def change_flag(self, num, change): 1053 | ''' Update the flag in the helper ''' 1054 | if self.exists: 1055 | tracker = self.trackers.get(num) 1056 | tracker.change(change) 1057 | cfg = self.tracker_configs.get(change) 1058 | if tracker.total == tracker.maximum + cfg.max_check: 1059 | self.update_batch(num, cfg.over_state) 1060 | elif not tracker.over: 1061 | lbl = self.lbls.get((num, tracker.total + cfg.tracked_num)) 1062 | lbl.config(fg=cfg.flag_state) 1063 | 1064 | def guessed_flag(self, num, guess_safe=None): 1065 | ''' Update the flags related to guesses (flag as OKAY or BLEW) on the helper ''' 1066 | if self.exists: 1067 | tracker = self.trackers.get(num) 1068 | tracker.lock() if guess_safe else tracker.blew() 1069 | revealed = tracker.blew_count + tracker.lock_count 1070 | lbl = self.lbls.get((num, revealed)) 1071 | lbl.config(fg=NumbHelper.FLAG_LOCK if guess_safe else NumbHelper.FLAG_BLEW) 1072 | if tracker.flag_count > 0: 1073 | for flag in range(tracker.flag_count): 1074 | try: 1075 | self.lbls.get((num, revealed + flag + 1)).config(fg=NumbHelper.FLAG_ACTIVE) 1076 | except AttributeError: 1077 | self.update_batch(num, NumbHelper.FLAG_OVER) 1078 | break 1079 | 1080 | def update_batch(self, num, colour): 1081 | ''' Batch update when tracker exceeds/resume from maximum ''' 1082 | for i in range(self.nrows * (4 if num >= 10 else 1)): 1083 | self.lbls.get((num, i + 1)).config(bg=colour) 1084 | 1085 | def destroy(self): 1086 | self.exists = False 1087 | super().destroy() 1088 | 1089 | def run(): 1090 | gui = GUI() 1091 | gui.run() 1092 | -------------------------------------------------------------------------------- /pyms/recorder.py: -------------------------------------------------------------------------------- 1 | ''' Record classes ''' 2 | 3 | # TODO - Check over the module to clear up any testing artifacts 4 | import os 5 | import pickle 6 | import tkinter as tk 7 | 8 | from tkinter.messagebox import askyesno, showerror 9 | from .constants import RECORD, MODES, MODE_CONFIG 10 | 11 | def get_mode(mode): 12 | ''' Convert to MODE_CONFIG if passed an int ''' 13 | if isinstance(mode, int): 14 | mode = MODES.get(mode) 15 | return mode 16 | 17 | class RecordKeeper: 18 | default_filepath = os.path.dirname(os.path.abspath(__file__)) 19 | default_filename = os.path.join(default_filepath, '.data.pms') 20 | 21 | @staticmethod 22 | def mode_str(mode: MODE_CONFIG): 23 | ''' Return the mode str ''' 24 | mode = get_mode(mode) 25 | return '{special}: {name} ({amount} IEDs @ {x}x{y})'.format( 26 | name=mode.name, 27 | special='Blackjack' if mode.special else 'Normal', 28 | amount=mode.amount, 29 | x=mode.x, 30 | y=mode.y 31 | ) 32 | 33 | def __init__(self, parent, records_to_keep: int = 10): 34 | self.parent = parent 35 | self._max = records_to_keep 36 | self.is_loaded = False 37 | 38 | 39 | def init_records(self): 40 | ''' Initialize all records ''' 41 | # Perhaps allow partially clearing by mode 42 | # If allow user to trigger, will need to import dialog. 43 | self.records = {RecordKeeper.mode_str(mode): [] for val, mode in MODES.items()} 44 | self.save() 45 | 46 | # The decorator needs to be static, so need to surpress the linter warning. 47 | # pylint: disable=no-self-argument 48 | def check_loaded(func): 49 | ''' A load checker decorator to handle file corruption issues ''' 50 | 51 | def checking(self, *args, **kwargs): 52 | # First, check if file is loaded 53 | if not self.is_loaded: 54 | showerror('Corrupted Records', 'Unable to load/save highscore data until cleared.') 55 | else: 56 | func(self, *args, **kwargs) # pylint: disable=not-callable 57 | return checking 58 | 59 | @check_loaded 60 | def show(self, current_mode=None): 61 | ''' Build the main window contents ''' 62 | # build the opt_mode menu 63 | self.window = tk.Toplevel(master=self.parent, padx=5, pady=5) 64 | self.window.title('Highscores') 65 | self.window.focus_force() 66 | self.window.grab_set() 67 | self.var_records = [RecordTkVar() for _ in range(self._max)] 68 | self.var_mode = tk.StringVar(master=self.window) 69 | self.var_mode.trace('w', self._update_entries) 70 | opt_mode = tk.OptionMenu( 71 | self.window, 72 | self.var_mode, 73 | *self.records.keys(), 74 | ) 75 | opt_mode.config(relief=tk.RIDGE) 76 | lbl_mode = tk.Label(master=self.window, text="Mode: ") 77 | self.window.grid_columnconfigure(index=0, weight=1) 78 | self.window.grid_columnconfigure(index=1, weight=2) 79 | lbl_mode.grid(row=0, column=0, sticky=tk.E) 80 | opt_mode.grid(row=0, column=1, sticky=tk.W) 81 | 82 | # build the records frame 83 | self.frm_main = tk.Frame(master=self.window, padx=5, pady=5) 84 | self.frm_main.grid(row=1, column=0, columnspan=2) 85 | self.build_records() 86 | btn_clear = tk.Button(master=self.window, text='CLEAR ALL RECORDS', command=self.clear_records) 87 | btn_clear.grid(row=2, column=0, columnspan=2) 88 | # self.__build_test_buttons() 89 | 90 | # set the default value 91 | if current_mode: 92 | self.var_mode.set(RecordKeeper.mode_str(current_mode)) 93 | else: 94 | # fall back scenario - though, shouldn't reach this point unless testing. 95 | self.var_mode.set(next(iter(self.records.keys()))) 96 | 97 | def __build_test_buttons(self): 98 | ''' buttons for testing ''' 99 | btn_save = tk.Button(master=self.window, text='SAVE', command=self.save) 100 | btn_load = tk.Button(master=self.window, text='LOAD', command=self.load) 101 | btn_clear = tk.Button(master=self.window, text='CLEAR ALL RECORDS', command=self.clear_records) 102 | btn_save.grid(row=2, column=0) 103 | btn_load.grid(row=2, column=1) 104 | btn_clear.grid(row=2, column=2) 105 | 106 | def clear_records(self): 107 | proceed = askyesno('Clearing all records...', 108 | 'Are you sure you want to clear all records?\nThis CANNOT be undone.') 109 | if proceed: 110 | self.init_records() 111 | self.var_mode.set(self.var_mode.get()) # trigger call back to refresh 112 | 113 | @check_loaded 114 | def add_record(self, mode: MODE_CONFIG, data): 115 | ''' Add record to mode ''' 116 | mode = get_mode(mode) 117 | records = self.records[RecordKeeper.mode_str(mode)] 118 | records.append(RecordEntry(mode, data)) 119 | records.sort(key=lambda record: record.sort_key()) 120 | records[:] = records[:self._max] 121 | self.save() 122 | 123 | def build_records(self): # pylint: disable=unused-argument 124 | ''' Build the individual records ''' 125 | frm = self.frm_main 126 | tk.Label(frm, text='♠ ⃞') # ??? If I don't add this line, somehow the combining unicode headers will mess up...?!?! 127 | headers = ['Rank', 'Seed', 'Time', 128 | '❓', '❗', '✨', 129 | 'Σ Hints', '⚑ Track', '♠ ⃞ Hits', 130 | '♥ ⃞ Rating'] 131 | stickys = [tk.W] + [tk.E] * 9 132 | justifys = [tk.LEFT] + [tk.RIGHT] * 9 133 | widths = [5, 10, 8] + [5] * 6 + [8] 134 | for row in range(self._max): 135 | if row == 0: 136 | for col, header in enumerate(headers): 137 | tk.Label(frm, text=header, justify=justifys[col]).grid(row=row, column=col, sticky=stickys[col]) 138 | else: 139 | for col in range(len(headers)): 140 | if col == 0: 141 | widget = tk.Label(frm, text=row) 142 | else: 143 | widget = tk.Entry(master=frm, 144 | textvariable=self.var_records[row-1][col-1], 145 | justify=justifys[col], state='readonly', 146 | relief=tk.FLAT, width=widths[col]) 147 | widget.grid(row=row, column=col, sticky=stickys[col]) 148 | 149 | def _update_entries(self, *args): 150 | ''' Update the record variables with the current mode records ''' 151 | records = self.records.get(self.var_mode.get(), []) 152 | records.sort(key=lambda record: record.sort_key()) 153 | gen_records = iter(records) 154 | # records = iter(self.records.get(self.var_mode.get(), [])) 155 | # for i, record in enumerate(records): 156 | # self.var_records[i].update(record) 157 | for var in self.var_records: 158 | # r = next(records, None) 159 | # var.update(r) 160 | var.update(next(gen_records, None)) 161 | 162 | def save(self, filename=None): 163 | if self.is_loaded: 164 | if not filename: 165 | filename = RecordKeeper.default_filename 166 | try: 167 | options = [opt.get() for opt in self.parent.options] 168 | except AttributeError: 169 | options = [] 170 | with open(filename, 'wb') as file: 171 | pickle.dump((self.records, options), file) 172 | 173 | def load(self, filename=None): 174 | if not filename: 175 | filename = RecordKeeper.default_filename 176 | try: 177 | with open(filename, 'rb') as file: 178 | self.records, options = pickle.load(file) 179 | self.is_loaded = True 180 | return options 181 | 182 | except FileNotFoundError: 183 | print('File not found, assuming empty records...') 184 | 185 | except pickle.UnpicklingError: 186 | result = askyesno('Corrupted', 187 | 'Records appear to be corrupted and cannot be loaded.\n\nClear ALL records and start fresh?') 188 | if not result: 189 | return None 190 | 191 | except Exception as e: # pylint: disable=broad-except,invalid-name 192 | # suppressing pylint for now, will test to see what exceptions can be expected 193 | print('Not sure what went wrong, why not take a look:\n{e}'.format(e=e)) 194 | return None 195 | 196 | # If no records are loaded for whatever reason, initiate the records unless stopped by users. 197 | if not self.is_loaded: 198 | self.init_records() 199 | self.is_loaded = True 200 | return None 201 | 202 | 203 | class RecordEntry: 204 | ''' 205 | RecordEntry class to manage additional functions from RECORD data 206 | 207 | Attributes: 208 | rating - provide a rating derived from the RECORD data 209 | sort_key() - provide a ranked index for sorting. 210 | ''' 211 | def __init__(self, mode: MODE_CONFIG, data: RECORD): 212 | self.data = data 213 | self.mode = get_mode(mode) 214 | 215 | @property 216 | def rating(self) -> float: 217 | ''' 218 | Calculate the rating to apply against the duration 219 | Based on the numerous factors from blackjack mode 220 | On normal, always return 1.0 as there's no differentiating factors 221 | ''' 222 | if self.mode.special: 223 | # Invert rate, i.e. the lower the initial values, the better the rating 224 | rate_guess = 1 - (self.data.IED_guesses / self.mode.amount) 225 | rate_hits = 1 - (self.data.IED_hits / 21) 226 | rate_blew = 1 - (self.data.IED_blew / 3 * (self.mode.amount // 52 + 3)) 227 | # the maximum possible blew amount are 9, 12, and 15 per 26, 52, and 104. 228 | mouseover = 1 - self.data.opt_mouseover 229 | tracker = 1 - self.data.opt_tracker 230 | hits_mode = 1 - (self.data.opt_allow_hits / 2) 231 | 232 | # Calculate the weighted rating 233 | weighted_rates = [ 234 | (rate_guess, .2), 235 | (rate_hits, .2), 236 | (rate_blew, .2), 237 | (mouseover, .05), 238 | (tracker, .05), 239 | (hits_mode, .3) 240 | ] 241 | return sum(r * w for r, w in weighted_rates) 242 | else: 243 | # Normal mode doesn't have any of these attributes to consider 244 | return 1.0 245 | 246 | def sort_key(self): 247 | ''' return the key for sorting ''' 248 | # Increase time by rating, so that there is a penalty to a lower rating. 249 | key = 2 * self.data.time_val - self.data.time_val * self.rating 250 | return key 251 | 252 | def __str__(self): 253 | return self.__repr__() 254 | 255 | def __repr__(self): 256 | return 'RecordEntry Object(time: {time_str}, rating: {rating}, data: {data})'.format( 257 | time_str=self.data.time_str, 258 | rating=self.rating, 259 | data=self.data 260 | ) 261 | 262 | class RecordTkVar: 263 | def __init__(self, n_data_to_show: int = 9): 264 | self.n = n_data_to_show 265 | self._default = '' 266 | self._vars = [tk.StringVar(value=self._default) for _ in range(self.n)] 267 | self.formatter = { 268 | 'opt_mouseover': ['☐', '☒'], #'☑'], 269 | 'opt_tracker': ['☐', '☒'], #'☑'], 270 | 'opt_allow_hits': ['⛔', '☕', '♿'], 271 | } 272 | 273 | def update(self, record: RecordEntry = None): 274 | ''' Update the inner tkvars ''' 275 | # print(type(record), RecordEntry, record.__class__, RecordEntry.__class__) 276 | if isinstance(record, RecordEntry): 277 | fields = record.data._fields 278 | for i, var in enumerate(self._vars): 279 | if i < 2: 280 | # Seed and time 281 | var.set(record.data[i+1]) 282 | elif record.mode.special: 283 | if i < self.n - 1: 284 | if fields[i+1].startswith('opt'): 285 | # use special format 286 | var.set(self.formatter.get(fields[i+1])[record.data[i+1]]) 287 | else: 288 | var.set(record.data[i+1]) 289 | else: 290 | # if the last record, show rating instead 291 | var.set('{:05.2f}%'.format(record.rating * 100)) 292 | else: 293 | var.set('-') 294 | else: 295 | self.clear() 296 | 297 | def clear(self): 298 | ''' Clear the inner tkvars ''' 299 | for var in self._vars: 300 | var.set(self._default) 301 | 302 | @property 303 | def values(self): 304 | return [var.get() for var in self._vars] 305 | 306 | def __getitem__(self, index): 307 | return self._vars[index] 308 | 309 | def _test(): 310 | ''' Unit testing ''' 311 | root = tk.Tk() 312 | def load(): 313 | keeper = RecordKeeper(root) 314 | keeper.show() 315 | btn = tk.Button(root, text='highscores', command=load) 316 | btn.pack() 317 | root.mainloop() 318 | 319 | if __name__ == '__main__': 320 | _test() 321 | -------------------------------------------------------------------------------- /pymsweeper.pyw: -------------------------------------------------------------------------------- 1 | from pyms.gui import run 2 | 3 | if __name__ == '__main__': 4 | run() 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup(name='pymsweeper', 4 | version='0.4', 5 | description='''"Oh yeah? I' gonna build my own Minesweeper, with blackjacks, and hookers! In fact, forget the hookers!"''', 6 | url='https://github.com/r-ook/pymsweeper', 7 | author='r.ook', 8 | author_email='regular.depression@gmail.com', 9 | keywords='pyms minesweeper blackjack gui tkinter tk mashup', 10 | license='GPLv3', 11 | packages=['pyms'], 12 | zip_safe=False) 13 | --------------------------------------------------------------------------------