├── .gitignore ├── LICENSE ├── README.md ├── dukedom ├── dukedom.py └── test_dukedom.py ├── hammurabi └── hammurabi.py └── wumpus └── wumpus.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | __pycache__ 21 | 22 | # Installer logs 23 | pip-log.txt 24 | 25 | # Unit test / coverage reports 26 | .coverage 27 | .tox 28 | nosetests.xml 29 | 30 | # Translations 31 | *.mo 32 | 33 | # Mr Developer 34 | .mr.developer.cfg 35 | .project 36 | .pydevproject 37 | -------------------------------------------------------------------------------- /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 | {one line to give the program's name and a brief idea of what it does.} 635 | Copyright (C) {year} {name of author} 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 | {project} Copyright (C) {year} {fullname} 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 | ## Hunt the Wumpus 2 | 3 | An implementation in Python 2.7 of the original "Hunt the Wumpus" game by 4 | Gregory Yob circa the mid 1970s, as described in 5 | [The Best of Creative Computing Volume 1](http://www.atariarchives.org/bcc1/showpage.php?page=247). This 6 | implementation remains true to the original game play, including bugs 7 | (features?) that are present in the original implementation. I've made only 8 | typographical adjustments and the addition of a 'q' key to quit. 9 | 10 | $ python wumpus/wumpus.py 11 | 12 | ## Hammurabi 13 | 14 | [Hammurabi](https://en.wikipedia.org/wiki/Hamurabi) is a game of city 15 | management, the text-based ancestor of games like Civilization. You play the 16 | role of ruler of Sumeria over a decade, during which your decision as to how 17 | much to feed your people, how much land to buy and sell and harvest, decide the 18 | fate of your city. Written in Python 3 and based on the 1978 implementation in 19 | [BASIC Computer Games](http://atariarchives.org/basicgames/showpage.php?page=78). 20 | 21 | $ python3 hammurabi/hammurabi.py 22 | 23 | ## Dukedom 24 | 25 | A more complex simulation of city management than Hammurabi, 26 | [Dukedom](http://en.wikipedia.org/wiki/Dukedom_%28game%29) 27 | places you as the Duke of a duchy, competing with other duchies to build your 28 | population and land, defeat them in battle, and ultimately challenge the King 29 | for the crown. Dukedom adds war, taxes, population moral, disease and a random 30 | generation system based on normal curves that means every game will be 31 | different. 32 | 33 | Based on the implementation in 34 | [Big Computer Games 1984](http://www.atariarchives.org/bigcomputergames/showpage.php?page=11) 35 | (see the same for playing instructions). Created by Vince Talbot in 1976, 36 | revised by Jamie Hanrahan and converted to Microsoft Basic by Richard Kaapke. 37 | 38 | $ python3 dukedom/dukedom.py 39 | -------------------------------------------------------------------------------- /dukedom/dukedom.py: -------------------------------------------------------------------------------- 1 | '''Dukedom, a more complex game of resource management than Hammurabi, with elements of empire building. 2 | 3 | Credits 4 | ------- 5 | Original concept by Vince Talbot in 1976, my implementation is based on the BASIC version that appears 6 | in Big Computer Games (1984). 7 | 8 | Instructions 9 | ------------ 10 | - Each peasant requires 13 hectolitres (HL) of grain per year to not starve. 11 | - It takes 2 HL of grain to plant 1 hectare (HA) of land. 12 | - If at the end of the year enough of your peasants have died that your population is reduced to a third 13 | of what it was at the start of the game, the High King will step in and take away your dukedom due to 14 | your mismanagement. 15 | 16 | Differences from the original 17 | ----------------------------- 18 | 19 | - Although the instructions in Big Computer Games states that a peasant can care for no more than 4 20 | hectares of land (when it comes to planting seed), this mechanic does not actually appear to be implmented 21 | in the BASIC version (it has the text, but not the check). 22 | 23 | - As 'Fruits of war' appears in both the land and grain detailed reports, I've replaced the land entry with 24 | 'Annexed land' and the grain entry with 'Captured grain'. 25 | 26 | - The BASIC version includes a "partially Gaussian random #" generator, which uses a uniform random number 27 | generator to produce numbers with a probability density function that (seems to very loosely) approximate 28 | that of a normal function. Python has a (couple of) random number generator(s) with normal distribution built 29 | in so I just use that. Then it's just working out the mean and standard deviation for each bell curve that Talbot 30 | intended. 31 | 32 | - Instructions for original claim 'yield for fallow land is calculated each year at random (variances in the weather) 33 | and ranges from 4 to 13 hectoliters for each hectare planted.' In fact the code shows that it uses a pdf with a mean 34 | between 4 and 9, and range of -2 to 3, giving a potential random number range of 2 to 12. The game then adds 9 to that 35 | number giving a yield range of 11 to 21 for fallow land! [C=FNX%(2)+9] 36 | 37 | - Instructions claim seven year locusts eat half the field grain, but in the BASIC code it's actually 35%. [C=INT(C*.65)] 38 | 39 | - Instructions claim 'A mercenary is worth about 8 peasants in fighting power', but from the code it actually seems 40 | to be worth 7. 41 | 42 | - It seems there's a bug in the original where if you lose a war and some of your land is annexed but all your land 43 | is 'poor land' (< 60% fertility) then the land won't be allocated out of your bucketed land, but will be taken 44 | from your total. 45 | 46 | TODO 47 | ---- 48 | - Check the land price probability distribution / calc, it's way off. 49 | - Co-routines to separate UI and simulation logic? 50 | - Occasionally get pdfs producing 0 when they shouldn't: curve 8 51 | - What happened to curve 7 - not used? 52 | ''' 53 | 54 | import argparse 55 | import collections 56 | from itertools import chain 57 | import math 58 | import random 59 | import textwrap 60 | 61 | 62 | def main(): 63 | parser = argparse.ArgumentParser() 64 | parser.add_argument('--talbot', dest='talbot', action='store_true') 65 | args = parser.parse_args() 66 | print('') 67 | print('D U K E D O M') 68 | print('') 69 | show_report = prompt_key('Do you want to skip detailed reports?', 'yn') == 'n' 70 | while True: 71 | try: 72 | dukedom(show_report, args.talbot) 73 | except EndGame as e: 74 | print(e) 75 | if prompt_key('Do you wish to play again?', 'yn') == 'n': 76 | break 77 | 78 | 79 | class GameReport: 80 | 81 | def __init__(self): 82 | self._data = collections.OrderedDict([ 83 | ('Peasants at start', 96 ), 84 | ('Starvations', 0 ), 85 | ('King\'s levy', 0 ), 86 | ('War casualties', 0 ), 87 | ('Looting victims', 0 ), 88 | ('Disease victims', 0 ), 89 | ('Natural deaths', -4 ), 90 | ('Births', 8 ), 91 | ('Peasants at end', 100 ), 92 | 93 | ('Land at start', 600 ), 94 | ('Bought/sold', 0 ), 95 | ('Annexed land', 0 ), 96 | ('Land at end of year', 600 ), 97 | 98 | ('Grain at start', 5193), 99 | ('Used for food', -1344), 100 | ('Land deals', 0 ), 101 | ('Seeding', -768 ), 102 | ('Rat losses', 0 ), 103 | ('Mercenary hire', 0 ), 104 | ('Captured grain', 0 ), 105 | ('Crop yield', 1516), 106 | ('Castle expense', -120 ), 107 | ('Royal tax', -300 ), 108 | ('Grain at end of year', 4177)]) 109 | 110 | def record(self, stat, x): 111 | self._data[stat] = x 112 | 113 | ZERO_EACH_YEAR = ['Starvations', 'King\'s Levy', 'Disease victims', 'Bought/sold', 114 | 'Land deals', 'Rat losses', 'Castle expense', 'War casualties', 115 | 'Looting victims', 'Annexed land', 'Captured grain', 'Royal tax'] 116 | 117 | def reset(self): 118 | for x in self.ZERO_EACH_YEAR: 119 | self._data[x] = 0 120 | 121 | def __iter__(self): 122 | return iter(self._data.items()) 123 | 124 | 125 | class GameState: 126 | 127 | def __init__(self): 128 | self.peasants = 100 129 | self.grain = 4177 # Hectolitres 130 | self.land = 600 # Hectares 131 | self.year = 0 132 | self.crop_yield = 3.95 133 | self.cool_down = 0 134 | self.resentment = 0 # long_term resentment trend 135 | self.buckets = [216, 200, 184, 0, 0, 0] # 100%, 80%, 60%, 40%, 20% and depleted land. 136 | 137 | 138 | def dukedom(show_report, use_talbot): 139 | if use_talbot: 140 | distributions = Talbot() 141 | else: 142 | distributions = Gaussian() 143 | 144 | report = GameReport() 145 | game = GameState() 146 | resentment = 0 147 | king = 0 148 | 149 | while True: 150 | report.record('Peasants at end', game.peasants) 151 | report.record('Land at end of year', game.land) 152 | report.record('Grain at end of year', game.grain) 153 | 154 | print('\nYear {} Peasants {} Land {} Grain {}\n'.format(game.year, game.peasants, game.land, game.grain)) 155 | if show_report: 156 | def group(it, n): 157 | for _ in range(n): 158 | label, x = next(it) 159 | if x: 160 | print(' {:<22}{}'.format(label, x)) 161 | print('') 162 | stats = iter(report) 163 | group(stats, 9) 164 | group(stats, 4) 165 | print(' 100% 80% 60% 40% 20% Depl') 166 | print((' ' + '{:>5}'*6).format(*game.buckets), '\n') 167 | group(stats, 11) 168 | if game.year <= 0: 169 | print('(Severe crop damage due to seven year locusts.)\n') 170 | 171 | # We start off in game year 0 for the first report. This is presumably to show continuity with 172 | # whoever was running the dukedom before, and add history to the game world. 173 | game.year = game.year + 1 174 | tax = 0 175 | levy = 0 176 | 177 | # Test for end game 178 | if game.peasants < 33: 179 | raise EndGame('pop loss') 180 | if game.land < 200: 181 | raise EndGame('land loss') 182 | if resentment > 88 or game.resentment > 99 or game.grain < 429: 183 | raise EndGame('deposed') 184 | if game.year > 45 and king == 0: 185 | raise EndGame('retirement') 186 | 187 | resentment = 0 188 | if king > 0: 189 | pay_tax = prompt_key('The King demands twice the royal tax in\n' 190 | 'THE HOPE TO PROVOKE WAR. WILL YOU PAY?', 'yn') 191 | if pay_tax == 'y': 192 | king = 2 193 | else: 194 | king = -1 195 | 196 | report.record('Peasants at start', game.peasants) 197 | report.record('Grain at start', game.grain) 198 | report.record('Land at start', game.land) 199 | report.reset() 200 | 201 | # Feed the peasants 202 | @validate_input 203 | def valid_food(x): 204 | if x > 100: 205 | if x > game.grain: 206 | raise NotEnoughGrain(game.grain) 207 | elif (x * game.peasants) > game.grain: 208 | raise NotEnoughGrain(game.grain) 209 | 210 | food = prompt_int('Grain for food = ', valid_food) 211 | 212 | # User can enter a number under 100 which represents food per peasant to give, 213 | # or a number over 100 which represents the total amount of food to give. 214 | if food > 100: 215 | food_per_capita = int(food / game.peasants) 216 | else: 217 | food_per_capita = food 218 | food = food * game.peasants 219 | 220 | if food_per_capita < 11 and food != game.grain: 221 | print('The peasants demonstrate before the castle.') 222 | 223 | game.grain -= food 224 | report.record('Used for food', -food) 225 | 226 | starved = 0 227 | overfed = 0 228 | if food_per_capita < 13: 229 | starved = game.peasants - int(food / 13) 230 | game.peasants -= starved 231 | print('Some peasants have starved') 232 | report.record('Starvations', -starved) 233 | overfed = min(4, food_per_capita - 14) 234 | resentment += (3 * starved) - (2 * overfed) 235 | 236 | if resentment > 88: 237 | raise EndGame('deposed') 238 | elif game.peasants < 33: 239 | raise EndGame('pop loss') 240 | 241 | # Buy and sell land 242 | bid = round(2 * game.crop_yield + distributions.random(1) - 5) 243 | 244 | @validate_input 245 | def valid_buy(x): 246 | if (x * bid) > game.grain: 247 | raise NotEnoughGrain(game.grain) 248 | 249 | bought = prompt_int('Land to buy at {0} HL./HA. = '.format(bid), valid_buy) 250 | 251 | if bought == 0: 252 | offer = bid - 1 253 | sellable = sum(game.buckets[:3]) 254 | 255 | @validate_input 256 | def valid_sell(x): 257 | if x > sellable: 258 | raise NotEnoughGoodLand(sellable) 259 | if (x * offer) > 4000: 260 | # You cannot sell more than 4000 HL worth of land in any one year. 261 | # That's all the grain available to pay you with. 262 | raise Overfill('No buyers have that much grain, try less') 263 | 264 | try: 265 | sold = prompt_int('Land to sell at {0} HL./HA. = '.format(offer), valid_sell, limit=3) 266 | except LimitExceeded: 267 | print('Buyers have lost interest.') 268 | sold = 0 269 | 270 | if sold: 271 | game.land -= sold 272 | 273 | # allocate sold land from good land starting at 60% and working up to 100% land 274 | x = list(reversed(game.buckets[:3])) 275 | sold_buckets = list(reversed(list(allocate(x, sold)))) 276 | game.buckets = [a - b for a, b in zip(game.buckets, chain(sold_buckets, [0, 0, 0]))] 277 | 278 | received = offer * sold 279 | 280 | if sold and offer < 4: 281 | print('The High King appropriates half of your earnings\nas punishment for selling at such a low price.') 282 | received = round(received / 2) 283 | 284 | game.grain += received 285 | report.record('Bought/sold', -sold) 286 | report.record('Land deals', received) 287 | else: 288 | game.land += bought 289 | game.buckets[2] += bought 290 | game.grain -= bid * bought 291 | report.record('Bought/sold', bought) 292 | report.record('Land deals', -bid * bought) 293 | 294 | # Farm land 295 | @validate_input 296 | def valid_farmland(land_to_farm): 297 | if land_to_farm > game.land: 298 | raise NotEnoughLand(game.land) 299 | elif (land_to_farm * 2) > game.grain: 300 | raise NotEnoughGrain(game.grain, hint=True) 301 | elif land_to_farm > (game.peasants * 4): 302 | raise NotEnoughWorkers(game.peasants) 303 | farmed = prompt_int('Land to be planted = ', valid_farmland) 304 | seeding = -(farmed * 2) 305 | game.grain += seeding 306 | report.record('Seeding', seeding) 307 | 308 | # War with the king 309 | if king == -2: 310 | mercs = math.floor(game.grain / 100) 311 | text = textwrap.dedent(''' 312 | The King\'s army is about to attack your duchy. 313 | At 100HL each (pay in advance) you have hired 314 | {0} foreign mercenaries. 315 | '''.format(mercs)).strip() 316 | print(text) 317 | if (mercs * 8) + game.peasants > 2399: 318 | raise EndGame('victory') 319 | else: 320 | raise EndGame('defeat') 321 | 322 | # Crop gains 323 | yld = distributions.random(2) + 9 324 | if (game.year % 7) == 0: 325 | # Field grain is eaten by seven year locusts. They eat half of all your crop 326 | # in the years that they appear. 327 | print('Seven year locusts.') 328 | yld = round(yld * 0.65) # Hmm, not really half... 329 | 330 | sown = list(allocate(game.buckets, farmed)) 331 | fallow = [a - b for a, b in zip(game.buckets, sown)] 332 | weighted = sum(area * (1.0 - (0.2 * i)) for i, area in enumerate(sown[:5])) 333 | if farmed > 0: 334 | game.crop_yield = round(yld * (weighted / farmed) * 100) / 100 335 | else: # avoid division by zero 336 | game.crop_yield = 0 337 | 338 | print('Yield = {} HL/HA.'.format(game.crop_yield)) 339 | 340 | depletion = [0] + sown[:4] + [sum(sown[4:])] 341 | nutrition = [sum(fallow[:3])] + fallow[3:] + [0, 0] 342 | game.buckets = [a + b for a, b in zip(depletion, nutrition)] 343 | 344 | # Crop losses 345 | crop_hazards = distributions.random(3) + 3 346 | if crop_hazards > 9: 347 | # Sometimes the rats get into the granary and eat up to 10% or so of your 348 | # reserve grain. Rats never eat field grain. 349 | eaten = round((crop_hazards * game.grain) / 83) 350 | print('Rats infest the grainery') 351 | game.grain -= eaten 352 | report.record('Rat losses', -eaten) 353 | 354 | if game.peasants > 66: 355 | levy = distributions.random(4) 356 | if levy < (game.peasants / 30): 357 | # Occasionally rats will eat so much of the High King's grain that some of his 358 | # workers starve to death. When this happens, the King will require some 359 | # peasants from each of his Dukes as replacements. You may supply them as 360 | # requested or pay an alternate amount of grain. 361 | or_grain = levy * 100 362 | msg = textwrap.dedent(''' 363 | The king requires {} peasants for 364 | his estate and mines. Will you supply 365 | them? (Y)es or pay {} HL. of 366 | grain instead (N)o?''').format(levy, or_grain).lstrip() 367 | if prompt_key(msg, 'yn') == 'n': 368 | game.grain -= or_grain 369 | tax = or_grain 370 | else: 371 | game.peasants -= levy 372 | report.record('King\'s levy', -levy) 373 | 374 | harvest = round(game.crop_yield * farmed) 375 | 376 | # war 377 | roll = distributions.random(5) 378 | desperation = max(2, round(11 - 1.5 * game.crop_yield)) # How badly neighbouring duchies are driven to attack 379 | war = War(distributions.random(6), game.peasants, resentment) 380 | 381 | if king == -1: 382 | king = -2 383 | print('The High King calls for peasant levies\nand hires many foreign mercenaries.') 384 | else: 385 | if roll < desperation: 386 | print('A nearby Duke threatens war.') 387 | 388 | if prompt_key('Will you attack first?', 'yn') == 'y': 389 | war.first_strike(desperation, roll) 390 | if war.ceasefire: 391 | print('Peace negotiations successful') 392 | crop_from_annexed_land = 0 393 | else: 394 | print('First strike failed - you need professionals.') 395 | 396 | if not war.ceasefire: 397 | 398 | @validate_input 399 | def validate_mercs(x): 400 | if x > 75: 401 | raise Overfill('There are only 75 available for hire.') 402 | mercs = prompt_int('How many mercenaries will you hire at 40HL. each = ', validate_mercs) 403 | 404 | won = war.campaign(mercs, game.grain) 405 | if won: 406 | if war.annexed > 399: 407 | print('You have overrun the enemy and annexed\n' 408 | 'his entire dukedom.') 409 | crop_from_annexed_land = round(war.annexed * 0.55) 410 | if king == 0: 411 | print('\nThe King fears for his throne and\n' 412 | 'may be planning direct action.') 413 | king = 1 414 | else: 415 | print('You have won the war.') 416 | # The crop you gain at the end of the year from land gained from the duchy that attacked you 417 | # is set at 0.67, presumably because the optimal way to farm land is to farm two-thirds of it 418 | # and to leave one-third fallow to gain nutrition; so we can assume that's what other duchies 419 | # are doing. 420 | crop_from_annexed_land = round(war.annexed * 0.67 * game.crop_yield) 421 | 422 | # Allocate annexed land equally between the three buckets of 'good' land. 423 | annexed = war.annexed 424 | res = [] 425 | for i in range(0, 3): 426 | x = round(annexed / (3 - i)) 427 | res.append(x) 428 | annexed -= x 429 | assert(annexed == 0) 430 | game.buckets = [a+b for a, b in zip(game.buckets, res + [0, 0, 0])] 431 | 432 | game.grain += war.captured_grain 433 | report.record('Captured grain', war.captured_grain) 434 | 435 | else: 436 | if war.annexed < -round(game.land * 0.67): 437 | raise EndGame('overrun') 438 | 439 | else: 440 | print('You have lost the war.') 441 | annexed_by_bucket = list(allocate(game.buckets[:3], abs(war.annexed), proportional=True)) 442 | game.buckets = [a-b for a, b in zip(game.buckets, annexed_by_bucket + [0, 0, 0])] 443 | 444 | # The amount of annexed land is a negative value here. 445 | crop_from_annexed_land = round(war.annexed * (farmed / game.land) * game.crop_yield) 446 | 447 | if war.looting_victims: 448 | print('There isn\'t enough grain to pay the mercenaries.') 449 | 450 | game.grain -= war.mercenary_pay 451 | 452 | game.peasants -= war.casualties + war.looting_victims 453 | game.land += war.annexed 454 | resentment += war.resentment 455 | 456 | harvest += crop_from_annexed_land 457 | 458 | report.record('War casualties', -war.casualties) 459 | report.record('Annexed land', war.annexed) 460 | report.record('Mercenary hire', -war.mercenary_pay) 461 | report.record('Looting victims', -war.looting_victims) 462 | 463 | # demographics 464 | deaths = 0 465 | chance_of_outbreak = distributions.random(8) + 1 466 | game.cool_down -= 1 467 | if chance_of_outbreak == 1 and game.cool_down == 0: 468 | print('The BLACK PLAGUE has struck the area') 469 | game.cool_down = 13 470 | deaths = -round(game.peasants / 3) 471 | elif chance_of_outbreak < 4: 472 | print('A POX EPIDEMIC has broken out') 473 | deaths = -round(game.peasants / (chance_of_outbreak * 5)) 474 | game.peasants += deaths 475 | report.record('Disease victims', deaths) 476 | 477 | natural_deaths = round(0.3 - game.peasants / 22) 478 | report.record('Natural deaths', natural_deaths) 479 | 480 | if war.looting_victims: 481 | birth_mod = 4.5 482 | else: 483 | birth_mod = distributions.random(8) + 4 484 | births = round(game.peasants / birth_mod) 485 | 486 | 487 | # Taxes and expenses 488 | 489 | if harvest > 4000: 490 | milling = round((harvest - 4000) * 0.1) 491 | else: 492 | milling = 0 493 | overhead = -120 494 | report.record('Castle expense', overhead - milling) 495 | 496 | if king >= 0: 497 | land_tax = round(game.land / 2) 498 | else: 499 | land_tax = 0 500 | 501 | if king >= 2: # royal tax is doubled 502 | land_tax *= 2 503 | if land_tax > game.grain: 504 | raise EndGame('beggared') 505 | 506 | report.record('Royal tax', -tax - land_tax) 507 | 508 | # end of year 509 | game.peasants += births + natural_deaths 510 | game.grain += harvest - milling - land_tax 511 | game.resentment = round(game.resentment * 0.85) + resentment 512 | 513 | report.record('Births', births) 514 | report.record('Crop yield', harvest) 515 | 516 | 517 | class War: 518 | 519 | """Calculate the outcome, casualties and resentment of war with a neighbouring duchy. 520 | 521 | - enemy_modifier: a random integer in the range [1, 9], is a proxy for enemy strength / size. 522 | - population: The number of peasants in your duchy. 523 | - resentment: an integer that gives the level of resentment against you by your peasants. 524 | """ 525 | def __init__(self, enemy_modifier, population, resentment): 526 | self.casualties = 0 527 | self.annexed = 0 528 | self.won = False 529 | self.mercenary_pay = 0 530 | self.looting_victims = 0 531 | self.captured_grain = 0 532 | self.ceasefire = False 533 | self.population = population 534 | self.resentment = 0 535 | 536 | mood = 1.2 - (resentment / 16.0) 537 | self.away = enemy_modifier * 18 + 85 538 | self.home = round(population * mood) + 13 539 | 540 | def first_strike(self, desperation, roll): 541 | """Strike first while the neighbouring duchy prepares for war. Done well you can force their 542 | hand into early 'peace negotiations', resulting in few casualties and a small level of resentment 543 | but no losses or gains for either of you. If the first strike fails, war will occur, however you 544 | will have additional losses and resentment due to the failed sortie. 545 | 546 | - desperation: A value between 2 and 11 that represents how badly the neighbouring duchy wants 547 | to go to war, due to bad crop yields. 548 | - roll: A random value for this war, between 3 and 9. 549 | """ 550 | self.ceasefire = self.home > self.away 551 | if self.ceasefire: 552 | self.casualties = 1 + desperation 553 | self.resentment = 2 * self.casualties 554 | else: 555 | self.casualties = 2 + desperation + roll 556 | self.away += (3 * self.casualties) 557 | 558 | def campaign(self, mercs, grain): 559 | """Fight the war. 560 | 561 | - mercs: a positive integer representing the number of mercenaries hired. 562 | 563 | Returns True if the campaign was won, False otherwise. Sets self.casualties with the total number 564 | of casualties since the war started. 565 | """ 566 | self.home += (mercs * 7) 567 | self.away = round(self.away * 1.95) 568 | casualties = round((self.away - (mercs * 4) - round(self.home * 0.25)) / 10) 569 | self.casualties += min(self.population-self.casualties, max(0, casualties)) 570 | self.annexed = round((self.home - self.away) * 0.8) 571 | self.won = self.home > self.away 572 | 573 | if self.won: 574 | self.landslide = self.annexed > 399 575 | if self.landslide: 576 | # We actually gain peasants from the population of the dukedom we've annexed. 577 | self.casualties = -47 578 | self.captured_grain = 3513 579 | else: 580 | self.captured_grain = round(self.annexed * 1.7) 581 | # Grain captured immediately from annexed land (not from harvest at the end of the year). This 582 | # can be used to pay mercenaries (unlike the harvest) and is the only form of credit in the game. 583 | grain += self.captured_grain 584 | 585 | pay = mercs * 40 586 | if pay > grain: 587 | self.mercenary_pay = grain 588 | looted = round((pay - grain) / 7) + 1 589 | self.looting_victims = min(self.population-self.casualties, looted) 590 | else: 591 | self.mercenary_pay = pay 592 | 593 | self.resentment = 2 * self.casualties + 3 * self.looting_victims 594 | return self.won 595 | 596 | 597 | def allocate(buckets, amount, proportional=False): 598 | n = len(buckets) 599 | for i, bucket in enumerate(buckets): 600 | if proportional: 601 | limit = round(bucket / (n - i)) 602 | else: 603 | limit = bucket 604 | x = min(amount, limit) 605 | amount = max(amount - x, 0) 606 | yield x 607 | 608 | 609 | def validate_input(validf): 610 | def wrapper(x): 611 | if x < 0: 612 | raise ValueError() 613 | validf(x) 614 | return x 615 | return wrapper 616 | 617 | 618 | def prompt_int(msg, valid, limit=None): 619 | i = 0 620 | while True: 621 | i += 1 622 | if limit and i > limit: 623 | raise LimitExceeded 624 | try: 625 | return valid(int(input(msg))) 626 | except InvalidInput as e: 627 | print(e) 628 | except ValueError: 629 | pass 630 | 631 | 632 | def prompt_key(msg, keys): 633 | while True: 634 | val = input(msg+' ').lower() 635 | if val in keys: 636 | return val 637 | 638 | 639 | class EndGame(RuntimeError): 640 | 641 | def __init__(self, reason): 642 | msg = { 643 | 'pop loss': 'You have so few peasants left that\n' 644 | 'the High King has abolished your Ducal\n' 645 | 'right.\n', 646 | 'deposed': 'The peasants are tired of war and starvation.\n' 647 | 'You are deposed.\n', 648 | 'land loss': 'You have so little land left that\n' 649 | 'the peasants are tired of war and starvation.\n' 650 | 'You are deposed.\n', 651 | 'retirement': 'You have reached the age of retirement.\n', 652 | 'overrun' : 'You have been overrun and and have lost\n' 653 | 'your entire Dukedom. Your head is placed\n' 654 | 'atop of the castle gate.\n', 655 | 'defeat' : 'Your head is placed atop of the castle gate.\n', 656 | 'victory' : 'Wipe the blood from the crown - you are High King!\n' 657 | 'A nearby monarchy THREATENS WAR! HOW MANY ......\n\n\n', 658 | 'beggared' : 'You have insufficient grain to pay\n' 659 | 'the royal tax.\n' 660 | }[reason] 661 | super().__init__(msg) 662 | 663 | 664 | class LimitExceeded(RuntimeError): 665 | 666 | pass 667 | 668 | 669 | class InvalidInput(ValueError): 670 | 671 | pass 672 | 673 | 674 | class NotEnoughGrain(InvalidInput): 675 | 676 | def __init__(self, grain, hint=False): 677 | msg = 'But you don\'t have enough grain.\nYou only have {} HL. of grain left.'.format(grain) 678 | if hint: 679 | msg += '\nEnough to plant {} HA. of land'.format(int(grain / 2)) 680 | super().__init__(msg) 681 | 682 | 683 | class NotEnoughLand(InvalidInput): 684 | 685 | def __init__(self, land): 686 | super().__init__('But you don\'t have enough land.\nYou only have {} HA. of land left.'.format(land)) 687 | 688 | 689 | class NotEnoughGoodLand(InvalidInput): 690 | 691 | def __init__(self, good_land): 692 | super().__init__('But you only have {} HA. of good land.'.format(good_land)) 693 | 694 | 695 | class NotEnoughWorkers(InvalidInput): 696 | 697 | def __init__(self, workers): 698 | super().__init__('But you don\'t have enough peasants to farm that land.\n' 699 | 'You only have enough to farm {} HA. of land.'.format(int(workers * 4))) 700 | 701 | class Overfill(InvalidInput): 702 | 703 | def __init__(self, msg): 704 | super().__init__(msg) 705 | 706 | 707 | class Gaussian: 708 | 709 | def __init__(self): 710 | self.means = [None] * 8 711 | self.means[0] = self._gauss(6.0, 1.0, 4, 8) 712 | self.means[1] = self._gauss(6.5, 1.1, 4, 9) 713 | self.means[2] = self._gauss(5.5, 0.9, 4, 7) # Chance of crop hazards 714 | self.means[3] = self._gauss(5.0, 1.1, 3, 7) # Chance of king's levy 715 | self.means[4] = self._gauss(6.0, 0.41, 5, 7) # Chance of war 716 | self.means[5] = self._gauss(5.0, 1.1, 3, 7) # Attacker's strength 717 | self.means[7] = self._gauss(5.0, 2.0, 1, 9) # Births 718 | 719 | def _gauss(self, mean, dev, a, b): 720 | return min(b, max(a, int(round(random.gauss(mean, dev))))) 721 | 722 | def random(self, curve): 723 | return self._gauss(0.5, 1.5, -3, 2) + self.means[curve-1] 724 | 725 | 726 | class Talbot: 727 | 728 | """Implements the "PARTIALLY GAUSSIAN RANDOM #" generator used by Talbot in his BASIC version 729 | of Dukedom. We have a (much) better Gaussian random number generator in Python, but I wanted 730 | to keep the option to use this around and I wanted to understand how Talbot's random number 731 | generation worked. I'm not sure how Talbot came up with this scheme - it doesn't seem based 732 | on any of the Gaussian approximation algorithm's I've researched from around that period.""" 733 | 734 | def __init__(self): 735 | self.table = [0] * 8 736 | self.init_table() 737 | 738 | def random(self, curve): 739 | return self.fnx(curve - 1) 740 | 741 | def fnr(self, a, b): 742 | """This function uses rounding to produce a very loose approximation of a normal distribution. 743 | It will produce a pseudo-random real number in the range [a, b + 1) with uniform redistribution. 744 | As the number is rounded to the nearest integer however, the lowest number a will only be rounded 745 | down to from a < x < (a + 0.5), and b will only be rounded up to from b > x > (b - 0.5), which is 746 | half the number range the other numbers can be rounded from, so the 2 end integers will be produced 747 | with half the probability of the other integers in the interval. 748 | 749 | >>> histogram = collections.Counter(fnr(4, 7) for _ in range(10000)) 750 | >>> print('fnr pdf ', sorted(histogram.most_common())) 751 | [(4, 1195), (5, 2497), (6, 2510), (7, 2553), (8, 1245)] 752 | 753 | Because this function produces integers in the interval [a, b+1), given that it 'approximates' a 754 | normal distribution we can see that the distibution has a mean of (a + b + 1) / 2. For example 755 | fnr(-2, 2) will produce a distribution with mean 0.5 (not 0); fnr(4, 7) will produce a distribution 756 | with mean 6 (not 5.5). 757 | """ 758 | return int(round(random.random() * (1 + b - a) + a)) 759 | 760 | def init_table(self): 761 | """Different stochastic properties within the game - births, crop yield, chance of disease, etc. - want 762 | to have different probability distributions. In this case while they all have the same standard deviation 763 | as the distribution fnr(-2, 2), we vary each curve at the start of the game by shifting its mean. TABLE 764 | is an array of the different mean transformation applied to each of the 8 probability curves used in the 765 | game. It's not the actual mean - that will actually be 0.5 higher (see fnr for explanation). An example 766 | initialization: 767 | 768 | [7, 8, 6, 4, 6, 4, 6, 2] 769 | 770 | The interesting part is that the mean shift itself for any given curve in any given game is produced 771 | with an approximately gaussian probability distribution: 772 | 773 | >>> histogram = collections.Counter() 774 | >>> for _ in range(10000): 775 | init_table() 776 | histogram.update([TABLE[0]]) 777 | >>> print(sorted(histogram.most_common())) 778 | [(4, 943), (5, 1687), (6, 4667), (7, 1709), (8, 994)] 779 | 780 | where TABLE[0] is calculated with the interval (4, 7). 781 | 782 | Experimentally, initializing the table 10,000 times and taking a histogram of the values produced for 783 | each curve gives something like this: 784 | 785 | (4, 7): [(4, 983), (5, 1710), (6, 4600), (7, 1765), (8, 942)] 786 | (4, 8): [(4, 636), (5, 1206), (6, 3736), (7, 1756), (8, 2300), (9, 366)] 787 | (4, 6): [(4, 1547), (5, 2834), (6, 4650), (7, 969)] 788 | (3, 6): [(3, 865), (4, 2924), (5, 2361), (6, 3017), (7, 833)] 789 | (5, 6): [(5, 1115), (6, 7800), (7, 1085)] 790 | (3, 6): [(3, 820), (4, 3036), (5, 2353), (6, 2985), (7, 806)] 791 | (3, 8): [(3, 367), (4, 1581), (5, 1372), (6, 3224), (7, 1373), (8, 1712), (9, 371)] 792 | (1, 8): [(1, 345), (2, 1114), (3, 949), (4, 1969), (5, 1256), (6, 1939), (7, 993), (8, 1054), (9, 381)] 793 | 794 | Notice how some of the distributions - (1, 8) and (3, 6) for example - have decidedly non-gaussian profiles 795 | with two peaks and a dip in between. 796 | """ 797 | pairs = [(4, 7), (4, 8), (4, 6), (3, 6), (5, 6), (3, 6), (3, 8), (1, 8)] 798 | for i, (a, b) in enumerate(pairs): 799 | r1 = self.fnr(a, b) 800 | if self.fnr(a, b) > 5: 801 | self.table[i] = int(round((r1 + self.fnr(a, b)) / 2)) 802 | else: 803 | self.table[i] = r1 804 | 805 | def fnx(self, a): 806 | """This function simply shifts the mean of the probability distribution produced by fnr(-2, 2) by the 807 | pre-calculated value in TABLE[a], and produces a random number from the resulting distribution. 808 | """ 809 | return self.fnr(-2, 2) + self.table[a] 810 | 811 | 812 | if __name__ == '__main__': 813 | main() 814 | -------------------------------------------------------------------------------- /dukedom/test_dukedom.py: -------------------------------------------------------------------------------- 1 | import dukedom 2 | import unittest 3 | 4 | 5 | class Tests(unittest.TestCase): 6 | 7 | def test_allocate(self): 8 | self.assertEqual(list(dukedom.allocate([1, 0, 0], 1)), [1, 0, 0]) 9 | self.assertEqual(list(dukedom.allocate([0, 1, 0], 1)), [0, 1, 0]) 10 | self.assertEqual(list(dukedom.allocate([0, 0, 1], 1)), [0, 0, 1]) 11 | self.assertEqual(list(dukedom.allocate([1, 0, 0], 0)), [0, 0, 0]) 12 | self.assertEqual(list(dukedom.allocate([1, 0, 0], 2)), [1, 0, 0]) 13 | self.assertEqual(list(dukedom.allocate([1, 1, 0], 2)), [1, 1, 0]) 14 | self.assertEqual(list(dukedom.allocate([1, 1, 0], 1)), [1, 0, 0]) 15 | self.assertEqual(list(dukedom.allocate([2, 0, 0], 1)), [1, 0, 0]) 16 | self.assertEqual(list(dukedom.allocate([1, 0, 1], 2)), [1, 0, 1]) 17 | self.assertEqual(list(dukedom.allocate([0, 0, 0], 1)), [0, 0, 0]) 18 | self.assertEqual(list(dukedom.allocate([10, 10, 10], 30)), [10, 10, 10]) 19 | self.assertEqual(list(dukedom.allocate([10, 10, 9 ], 30)), [10, 10, 9 ]) 20 | self.assertEqual(list(dukedom.allocate([10, 10, 10], 29)), [10, 10, 9 ]) 21 | self.assertEqual(list(dukedom.allocate([10, 10, 10], 15, proportional=True)), [3, 5, 7]) 22 | self.assertEqual(list(dukedom.allocate([10, 0, 0], 15, proportional=True)), [3, 0, 0]) 23 | self.assertEqual(list(dukedom.allocate([ 0, 10, 0], 15, proportional=True)), [0, 5, 0]) 24 | self.assertEqual(list(dukedom.allocate([ 0, 0, 10], 15, proportional=True)), [0, 0, 10]) 25 | self.assertEqual(list(dukedom.allocate([ 0, 0, 0], 15, proportional=True)), [0, 0, 0]) 26 | self.assertEqual(list(dukedom.allocate([10, 10, 10], 2, proportional=True)), [2, 0, 0]) 27 | self.assertEqual(list(dukedom.allocate([10, 10, 10], 4, proportional=True)), [3, 1, 0]) 28 | self.assertEqual(list(dukedom.allocate([10, 10, 10], 9, proportional=True)), [3, 5, 1]) 29 | self.assertEqual(list(dukedom.allocate([10, 10, 10], 20, proportional=True)), [3, 5, 10]) 30 | 31 | 32 | class WarTests(unittest.TestCase): 33 | 34 | def _test(self, attr, expected): 35 | for enemy in range(1, 10): 36 | war = dukedom.War(enemy, 150, -10) 37 | war.campaign(10, 0) 38 | self.assertEqual(getattr(war, attr), expected[0][enemy-1], msg='enemy {}'.format(enemy)) 39 | 40 | for i, pop in enumerate(range(80, 160, 10)): 41 | war = dukedom.War(5, pop, -10) 42 | war.campaign(15, 0) 43 | self.assertEqual(getattr(war, attr), expected[1][i], msg='pop {}'.format(pop)) 44 | 45 | for i, resentment in enumerate(range(-40, 40, 10)): 46 | war = dukedom.War(5, 150, resentment) 47 | war.campaign(0, 0) 48 | self.assertEqual(getattr(war, attr), expected[2][i], msg='resentment {}'.format(resentment)) 49 | 50 | for i, mercs in enumerate(range(0, 71, 10)): 51 | war = dukedom.War(5, 100, 0) 52 | war.campaign(mercs, 0) 53 | self.assertEqual(getattr(war, attr), expected[3][i], msg='mercenaries {}'.format(mercs)) 54 | 55 | def test_outcome(self): 56 | """Test the outcome of war along the four dimensions.""" 57 | expected = [ 58 | [ True, True, True, True, True, False, False, False, False], 59 | [False, False, False, False, False, True, True, True, True], 60 | [ True, True, True, False, False, False, False, False, False], 61 | [False, False, False, True, True, True, True, True]] 62 | self._test('won', expected) 63 | 64 | def test_casualties(self): 65 | expected = [ 66 | [7, 11, 14, 18, 21, 25, 28, 32, 35], 67 | [22, 21, 21, 20, 20, 19, 19, 18, 18], 68 | [20, 22, 25, 27, 29, 32, 34, 36, 39], 69 | [31, 25, 19, 14, 8, 2, 0, 0]] 70 | self._test('casualties', expected) 71 | 72 | population = 30 73 | war = dukedom.War(9, population, 50) 74 | war.campaign(0, 0) 75 | self.assertEqual(war.casualties, population) 76 | 77 | def test_land_annexation(self): 78 | expected = [ 79 | [ 125, 97, 69, 41, 13, -15, -43, -72, -100], 80 | [ -62, -47, -33, -18, -3, 11, 26, 41, 55], 81 | [ 182, 106, 32, -43, -118, -194, -269, -343, -418], 82 | [-166, -110, -54, 2, 58, 114, 170, 226]] 83 | self._test('annexed', expected) 84 | 85 | def test_mercenary_pay(self): 86 | war = dukedom.War(5, 100, 0) 87 | for mercs in range(0, 71, 10): 88 | war.campaign(mercs, 10000) 89 | self.assertEqual(war.mercenary_pay, 40 * mercs) 90 | 91 | def test_looting_victims(self): 92 | vals = [(41, 0), (40, 0), (39, 1), (37, 1), (36, 2), (30, 2), (29, 3)] 93 | for grain, expected in vals: 94 | war = dukedom.War(5, 100, 0) 95 | war.campaign(1, grain) 96 | self.assertEqual(war.looting_victims, expected) 97 | 98 | def test_captured_grain_pays_mercenaries(self): 99 | """Test that grain captured from the enemy during victory can be used to pay mercenaries.""" 100 | vals = [(160, 25, 0), (160, 23, 1), (160, 20, 2)] 101 | # In this war we win by just enough to capture 15 HL. of grain from the enemy. 102 | for pop, grain, looted in vals: 103 | war = dukedom.War(1, pop, 0) 104 | war.campaign(1, grain) 105 | self.assertEqual(war.looting_victims, looted) 106 | 107 | def test_captured_grain(self): 108 | """Test that victory results in grain captured immediately from annexed land.""" 109 | expected = [ 110 | [212, 165, 117, 70, 22, 0, 0, 0, 0], 111 | [ 0, 0, 0, 0, 0, 19, 44, 70, 94], 112 | [309, 180, 54, 0, 0, 0, 0, 0, 0], 113 | [ 0, 0, 0, 3, 99, 194, 289, 384]] 114 | self._test('captured_grain', expected) 115 | 116 | def test_landslide_victory(self): 117 | war = dukedom.War(1, 200, -20) 118 | war.campaign(50, 10000) 119 | self.assertTrue(war.landslide) 120 | self.assertEqual(war.casualties, -47) 121 | self.assertEqual(war.captured_grain, 3513) 122 | self.assertEqual(war.looting_victims, 0) 123 | 124 | def test_successful_first_strike(self): 125 | """Test the outcome of a successful first strike, and the casualties and resentment it produces.""" 126 | values = [(2, 3, 6), (6, 7, 14), (11, 12, 24)] 127 | for desperation, casualties, resentment in values: 128 | war = dukedom.War(1, 200, -20) 129 | war.first_strike(desperation, 3) 130 | self.assertTrue(war.ceasefire) 131 | self.assertEqual(war.casualties, casualties) 132 | self.assertEqual(war.resentment, resentment) 133 | 134 | def test_failed_first_strike_outcome(self): 135 | war = dukedom.War(9, 100, 20) 136 | war.first_strike(2, 3) 137 | self.assertFalse(war.ceasefire) 138 | 139 | def test_failed_first_strike_casualties(self): 140 | """Test the outcome of a failed first strike and the casualties it produces. 141 | We don't test the resentment produced as this will be calculated after the war, which is now 142 | inevitable. However the level of failure of the first strike does affect that resentment, but 143 | there is a seperate test for that connection. 144 | """ 145 | values = [(2, 7), (6, 11), (11, 16)] 146 | for desperation, casualties in values: 147 | war = dukedom.War(9, 100, 20) 148 | war.first_strike(desperation, 3) 149 | self.assertEqual(war.casualties, casualties) 150 | 151 | values = [(3, 7), (6, 10), (9, 13)] 152 | for roll, casualties in values: 153 | war = dukedom.War(9, 100, 20) 154 | war.first_strike(2, roll) 155 | self.assertEqual(war.casualties, casualties) 156 | 157 | def test_failed_first_strike_increases_enemy_strength(self): 158 | values = [(2, 3, 268), (11, 9, 313)] 159 | for desperation, roll, enemy_strength in values: 160 | war = dukedom.War(9, 100, 20) 161 | before = war.away 162 | war.first_strike(desperation, roll) 163 | self.assertGreater(war.away, before) 164 | self.assertEqual(war.away, enemy_strength) 165 | 166 | def test_failed_first_strike_accounted_for_in_campaign_accounting(self): 167 | war = dukedom.War(5, 100, 0) 168 | war.first_strike(2, 3) 169 | war.campaign(0, 0) 170 | self.assertEqual(war.casualties, 42) 171 | 172 | if __name__ == '__main__': 173 | unittest.main() 174 | # unittest.TextTestRunner().run(unittest.TestLoader().loadTestsFromName('test_dukedom.WarTests.test_captured_grain_pays_mercenaries')) -------------------------------------------------------------------------------- /hammurabi/hammurabi.py: -------------------------------------------------------------------------------- 1 | """The classic game of Hammurabi. 2 | 3 | BUGS: 4 | - Mortality calc rates seem wrong at end of game. 5 | """ 6 | 7 | from random import randint, choice, random 8 | import textwrap 9 | import math 10 | 11 | 12 | class EndGame(RuntimeError): 13 | 14 | pass 15 | 16 | 17 | class Impeached(EndGame): 18 | 19 | def __init__(self, starved): 20 | super().__init__('You starved {} people in one year!!!'.format(starved)) 21 | 22 | 23 | class Resigns(EndGame): 24 | 25 | def __init__(self): 26 | super().__init__('\nHammurabi: I cannot do what you wish.\nGet yourself another steward!!!') 27 | 28 | 29 | def hammurabi(): 30 | harvest = 3000 31 | grain = 2800 32 | ratfood = 200 33 | _yield = 3 # bushels per acre 34 | acres = int(math.floor(harvest / _yield)) 35 | pop = 100 36 | born = 5 37 | starved = 0 38 | 39 | impeached = False 40 | plague = False 41 | 42 | total_deaths = 0 43 | mortality_rate = 0 44 | 45 | print('Try your hand at governing ancient Sumeria\n' 46 | 'for a ten-year term of office.') 47 | 48 | try: 49 | for year in range(1, 11): 50 | if plague: 51 | pop = int(pop / 2) 52 | plague_text = '\na horrible plague struck! Half the people died,' 53 | else: 54 | plague_text = '' 55 | 56 | print(textwrap.dedent(''' 57 | Hammurabi: I beg to report to you, 58 | in year {}, {} people starved, {} came to the city,{} 59 | population is now {} 60 | the city now owns {} acres. 61 | You harvested {} bushels per acre. 62 | Rats ate {} bushels. 63 | You now have {} bushels in store. 64 | ''').format(year, starved, born, plague_text, pop, acres, _yield, ratfood, grain)) 65 | 66 | price = randint(17, 26) 67 | print('Land is trading at {} bushels per acre.'.format(price)) 68 | 69 | def prompt(msg, is_valid, fail_msg): 70 | while True: 71 | try: 72 | amt = int(input(msg + ' ')) 73 | except ValueError: 74 | continue 75 | if amt < 0: 76 | raise Resigns() 77 | if is_valid(amt): 78 | return amt 79 | print(fail_msg) 80 | 81 | buy = prompt('How many acres do you wish to buy (0-{})?'.format(int(grain / price)), 82 | lambda n: n * price <= grain, 83 | 'Hammurabi: Think again. You have only {} bushels of grain. Now then,'.format(grain)) 84 | 85 | if buy > 0: 86 | acres += buy 87 | grain -= price * buy 88 | elif buy == 0: 89 | sell = prompt('How many acres do you wish to sell (0-{})?'.format(acres), 90 | lambda n: n <= acres, 91 | 'Hammurabi: Think again. You own only {} acres. Now then,'.format(acres)) 92 | if sell > 0: 93 | acres -= sell 94 | grain += (sell * price) 95 | 96 | feed = prompt('How many bushels do you wish to feed your people (0-{})?'.format(grain), 97 | lambda n: n <= grain, 98 | 'Hammurabi: Think again. You have only {} bushels of grain. Now then,'.format(grain)) 99 | grain = grain - feed 100 | 101 | while True: 102 | limit = min(acres, grain * 2, pop * 10) 103 | planted = int(input('How many acres do you wish to plant (0-{})? '.format(limit))) 104 | if planted < 0: 105 | raise Resigns() 106 | if planted > acres: 107 | print('Hammurabi: Think again. You own only {} acres. Now then,'.format(acres)) 108 | continue 109 | if (planted / 2) > grain: # 1 bushel plants 2 acres 110 | print('Hammurabi: Think again. You have only {} bushels. Now then,'.format(grain)) 111 | continue 112 | if planted > (10 * pop): # 1 person can tend 10 acres 113 | print('Hammurabi: But you have only {} people to tend the fields! Now then,'.format(pop)) 114 | continue 115 | break 116 | 117 | grain -= int(math.ceil(planted / 2)) 118 | _yield = randint(1, 6) 119 | harvest = planted * _yield 120 | ratfood = int(grain / choice([2, 4, 6])) if bool(choice([0, 1])) else 0 121 | grain = grain + harvest - ratfood 122 | born = int(randint(1, 6) * (20 * acres + grain) / float(pop) / 100.0 + 1) 123 | fed = int(feed / 20.0) 124 | starved = pop - fed 125 | pop = pop - starved + born 126 | plague = random() < 0.15 127 | total_deaths += starved 128 | mortality_rate = ((year - 1) * mortality_rate + starved * 100 / pop) / year 129 | 130 | if starved > (pop * 0.45): 131 | raise Impeached(starved) 132 | 133 | except Impeached as e: 134 | print(e) 135 | impeached = True 136 | except Resigns as e: 137 | print(e) 138 | return 139 | 140 | acres_per_person = acres / pop 141 | 142 | print() 143 | print('In your {}-year term of office, {} percent of the\n' 144 | 'population starved per year on the average, i.e. a total of\n' 145 | '{} people died!!\n'.format(year, int(round(mortality_rate)), total_deaths)) 146 | 147 | if impeached or mortality_rate > 33 or acres_per_person < 7: 148 | print('Due to your extreme mismanagement you have not only\n' 149 | 'been impeached and thrown out of office but you have\n' 150 | 'also been declared national fink!!!!') 151 | elif mortality_rate > 10 or acres_per_person < 9: 152 | print('Your heavy-handed performance smacks of Nero and Ivan IV.\n' 153 | 'The people (remaining) find you an unpleasant ruler, and,\n' 154 | 'frankly, hate your guts!!') 155 | elif mortality_rate > 3 or acres_per_person < 10: 156 | haters = int(pop * random() * 0.8) 157 | print('Your performance could have been somewhat better, but\n' 158 | 'really wasn\'t too bad at all. {} people\n' 159 | 'dearly like to see you assassinated but we all have our\n' 160 | 'trivial problems.'.format(haters)) 161 | else: 162 | print('A fantastic performance!!! Charlemagne, Disraeli and\n' 163 | 'Jefferson combined could not have done better!') 164 | 165 | print('\nSo long for now.') 166 | 167 | 168 | if __name__ == '__main__': 169 | hammurabi() -------------------------------------------------------------------------------- /wumpus/wumpus.py: -------------------------------------------------------------------------------- 1 | """Hunt the Wumpus 2 | 3 | http://www.atariarchives.org/morebasicgames/showpage.php?page=179 4 | 5 | - Bugs (in the original game also): 6 | - You can repeatedly put the same room for the arrow 7 | - No range checking on the input for rooms for the arrow 8 | """ 9 | 10 | from copy import deepcopy 11 | import random 12 | import textwrap 13 | 14 | 15 | cave = { 16 | 1: (2,5,8), 17 | 2: (1,3,10), 18 | 3: (2,4,12), 19 | 4: (3,5,14), 20 | 5: (1,4,6), 21 | 6: (5,7,15), 22 | 7: (6,8,17), 23 | 8: (1,7,9), 24 | 9: (8,10,18), 25 | 10: (2,9,11), 26 | 11: (10,12,19), 27 | 12: (3,11,13), 28 | 13: (12,14,20), 29 | 14: (4,13,15), 30 | 15: (6,14,16), 31 | 16: (15,17,20), 32 | 17: (7,16,18), 33 | 18: (9,17,19), 34 | 19: (11,18,20), 35 | 20: (13,16,19)} 36 | 37 | 38 | instructions = textwrap.dedent(""" 39 | Welcome to 'Hunt the Wumpus'. 40 | 41 | The wumpus lives in a cave of 20 rooms. Each room has 3 tunnels 42 | leading to other rooms. (Look at a dodecahedron to see how this works 43 | - if you don't know what a dodecahedron is, ask someone). 44 | 45 | Hazards: 46 | Bottomless Pits - two rooms have bottomless pits in them. If you go 47 | there, you fall into the pit (and lose!) 48 | 49 | Super Bats - two other rooms have super bats. If you go there, a 50 | bat grabs you and takes you to some other room at random (which 51 | might be troublesome). 52 | 53 | Wumpus: 54 | The wumpus is not bothered by the hazards (he has sucker feet and 55 | is too big for a bat to lift). Usually he is asleep. Two things 56 | wake him up: your entering his room or your shooting an arrow. 57 | 58 | If the wumpus wakes, he moves (p=.75) one room or stays still 59 | (p=.25). After that, if he is where you are, he eats you up (and you 60 | lose!) 61 | 62 | You: 63 | Each turn you may move or shoot a crooked arrow. 64 | 65 | Moving: you can go one room (through one tunnel). 66 | 67 | Arrows: you have 5 arrows. you lose when you run out. Each arrow 68 | can go from 1 to 5 rooms. You aim by telling the computer the 69 | room #s you want the arrow to go to. If the arrow can't go that 70 | way (ie no tunnel) it moves at random to the next room. If the 71 | arrow hits the wumpus, you win. If the arrow hits you, you 72 | lose. 73 | 74 | Warnings: 75 | When you are one room away from the wumpus or a hazard, the computer 76 | says: 77 | 78 | Wumpus - 'I smell a wumpus' 79 | Bat - 'Bats nearby' 80 | Pit - 'I feel a draft'""") 81 | 82 | 83 | class GameState: 84 | 85 | def __init__(self): 86 | rooms = range(1, 21) 87 | random.shuffle(rooms) 88 | spawn = iter(rooms) 89 | self.wumpus = spawn.next() 90 | self.hunter = spawn.next() 91 | self.bats = [spawn.next() for _ in range(2)] 92 | self.pits = [spawn.next() for _ in range(2)] 93 | self.arrows = 5 94 | self.playing = True 95 | self.won = False 96 | 97 | 98 | def prompt(msg, keys=None): 99 | """Prompt the player for char input, optionally restricted to a set of chars.""" 100 | if keys: 101 | msg += ' ({0})'.format('-'.join(c for c in keys)) 102 | while True: 103 | val = raw_input(msg + '? ').lower() 104 | if val in keys or not keys: 105 | return val 106 | 107 | 108 | def prompt_int(msg, rng=None): 109 | """Prompt the player for numeric input, optionally between a given [min, max] range (inclusive).""" 110 | if rng: 111 | msg += ' ({0}-{1})'.format(*rng) 112 | while True: 113 | try: 114 | val = int(raw_input(msg + '? ')) 115 | if (rng and rng[0] <= val <= rng[1]) or not rng: 116 | return val 117 | except ValueError: 118 | pass 119 | 120 | 121 | def move_wumpus(game): 122 | if random.random() > 0.25: 123 | game.wumpus = random.choice(cave[game.wumpus]) 124 | 125 | 126 | def hunt_the_wumpus(): 127 | quitting = False 128 | last_game = None 129 | reset = True 130 | 131 | if prompt('Instructions', 'yn') == 'y': 132 | print instructions 133 | 134 | while not quitting: 135 | if reset: 136 | game = GameState() 137 | last_game = deepcopy(game) 138 | else: 139 | game = deepcopy(last_game) 140 | 141 | playing = True 142 | won = False 143 | 144 | print 145 | print 'Hunt the Wumpus' 146 | print 147 | 148 | while playing: 149 | neighbouring_caves = cave[game.hunter] 150 | if game.wumpus in neighbouring_caves: 151 | print 'I smell a wumpus!' 152 | if any(bat in neighbouring_caves for bat in game.bats): 153 | print 'Bats nearby!' 154 | if any(pit in neighbouring_caves for pit in game.pits): 155 | print 'I feel a draught.' 156 | print 'You are in room', game.hunter 157 | print 'Tunnels lead to {0}, {1}, {2}'.format(*neighbouring_caves) 158 | 159 | command = prompt('Shoot, Move or Quit', 'smq') 160 | 161 | if command == 'q': 162 | playing = False 163 | quitting = True 164 | 165 | if command == 'm': 166 | while True: 167 | to = prompt_int('Where to') 168 | if to in neighbouring_caves: 169 | game.hunter = to 170 | break 171 | else: 172 | print 'Not possible -' 173 | 174 | if game.hunter in game.bats: 175 | print 'Zap -- super bat snatch! Elsewhereville for you!' 176 | game.hunter = random.randrange(0, 21) 177 | elif game.hunter in game.pits: 178 | print 'YYYIIIIEEEE... fell in pit' 179 | playing = False 180 | elif game.hunter == game.wumpus: 181 | print '... oops! Bumped a wumpus' 182 | move_wumpus(game) 183 | if game.hunter == game.wumpus: 184 | print 'Tsk, tsk, tsk - Wumpus got you!' 185 | playing = False 186 | 187 | print 188 | 189 | elif command.lower() == 's': 190 | n = prompt_int('No. of rooms', [1, 5]) 191 | path = [] 192 | while len(path) < n: 193 | room = prompt_int('Room') 194 | if (len(path) == 1 and room == game.hunter) or (len(path) > 1 and room == path[-2]): 195 | print 'Arrows aren\'t that crooked - try another room' 196 | else: 197 | path.append(room) 198 | 199 | curr = game.hunter 200 | for room in path: 201 | if room in cave[curr]: 202 | if room == game.hunter: 203 | print 'Ouch! Arrow got you!' 204 | playing = False 205 | break 206 | elif room == game.wumpus: 207 | print 'Aha! You got the wumpus!' 208 | playing = False 209 | won = True 210 | break 211 | curr = room 212 | else: 213 | curr = random.choice(cave[curr]) 214 | else: 215 | print 'Missed' 216 | 217 | if not won: 218 | game.arrows -= 1 219 | if not game.arrows: 220 | playing = False 221 | 222 | move_wumpus(game) 223 | if game.hunter == game.wumpus: 224 | print 'Tsk, tsk, tsk - wumpus got you!' 225 | playing = False 226 | 227 | print 228 | 229 | if not quitting: 230 | if won: 231 | print 'Hee hee hee - the Wumpus\'ll getcha next time!!' 232 | else: 233 | print 'Ha ha ha - you lose!' 234 | 235 | reset = prompt('Same set-up', 'yn') == 'n' 236 | print 237 | 238 | 239 | if __name__ == '__main__': 240 | hunt_the_wumpus() 241 | --------------------------------------------------------------------------------