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