├── .gitignore ├── LICENSE ├── README.md ├── advanced ├── 05-extract-module │ ├── space_game.py │ ├── test_space_game.py │ └── text_en.py ├── 06-extract-function │ ├── space_game.py │ ├── test_space_game.py │ └── text_en.py ├── 07-extract-and-modify │ ├── space_game.py │ ├── test_space_game.py │ └── text_en.py ├── 08-extract-data-structures │ ├── space_game.py │ ├── test_space_game.py │ └── text_en.py ├── 09-extract-class │ ├── space_game.py │ ├── test_space_game.py │ └── text_en.py ├── 10-final-cleanup │ ├── space_game.py │ ├── test_space_game.py │ └── text_en.py ├── space_game.py └── test_space_game.py ├── solution ├── 01-extract-module │ ├── space_game.py │ ├── test_space_game.py │ └── text_en.py ├── 02-extract-function │ ├── space_game.py │ ├── test_space_game.py │ └── text_en.py ├── 03-extract-and-modify │ ├── space_game.py │ ├── test_space_game.py │ └── text_en.py ├── 04-extract-data-structure │ ├── space_game.py │ ├── test_space_game.py │ └── text_en.py ├── 05-extract-class │ ├── puzzles.py │ ├── space_game.py │ ├── test_space_game.py │ └── text_en.py ├── 06-another-class │ ├── puzzles.py │ ├── space_game.py │ ├── test_space_game.py │ └── text_en.py └── 07-oop-decouple-game-logic │ ├── puzzles.py │ ├── space_game.py │ ├── test_space_game.py │ └── text_en.py ├── solution_jan24 ├── space_game.py ├── space_game_chatgpt.py ├── space_game_final.py ├── test_space_game.py └── text_en.py ├── solution_pycon22 ├── space_game.py ├── test_space_game.py └── text_en.py ├── space_game.py ├── talk ├── PRESENT.md ├── abstract.md ├── four_icon_cv.png ├── starmap.png └── starmap.svg └── test_space_game.py /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .idea/* 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | pip-wheel-metadata/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Kristian Rother 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Refactoring 101 3 | 4 | **You find material for the 2024 PyconDE Tutorial on [https://github.com/krother/space](https://github.com/krother/space)** 5 | 6 | ![](talk/starmap.png) 7 | 8 | *planet images by [Justin Nichol on opengameart.org](https://opengameart.org/content/20-planet-sprites) CC-BY 3.0* 9 | 10 | ## Goal of this Tutorial 11 | 12 | In this tutorial, you will refactor a space travel text adventure. 13 | 14 | Starting with a working but messy program, you will improve the structure of the code. 15 | Throughout the tutorial, you will apply standard techniques that make your code more readable and easier to maintain. 16 | This tutorial is suitable for junior Python developers. 17 | 18 | ---- 19 | 20 | ## 1. What is Refactoring? 21 | 22 | When you are working on your first real-world Python project, the codebase is typically much larger than any textbook or course example. Over time, software entropy kicks in: functions grow longer and longer, the same code gets copy-pasted to multiple places and slowly mutates there, and code that seemed a brilliant idea a few weeks back is now incomprehensible. Refactoring aims to prevent your code from becoming a mess. 23 | 24 | **Refactoring is improving the structure of code without changing its functionality.** 25 | 26 | In practice, this means thing like: 27 | 28 | * remove redundant code segments 29 | * split long functions into shorter ones 30 | * extract data structures 31 | * encapsulate behavior into classes 32 | 33 | In this tutorial, you can try all of these. Let's go! 34 | 35 | ### 1.1 Recipe: Generic Refactoring 36 | 37 | The basic workflow in refactoring is: 38 | 39 | 1. run the tests 40 | 2. edit the code 41 | 3. run the tests 42 | 43 | ---- 44 | 45 | ## 2. Getting Started 46 | 47 | Clone or download the space travel game from [github.com/krother/refactoring_tutorial](https://github.com/krother/refactoring_tutorial): 48 | 49 | git clone git@github.com:krother/refactoring_tutorial.git 50 | 51 | The game is a text-based command-line app that should run in any Python editor/environment. 52 | Make sure it runs: 53 | 54 | python space_game.py 55 | 56 | Play the game for a few minutes to get a feeling what it is about. 57 | 58 | ---- 59 | 60 | ## 3. Run the Tests 61 | 62 | A fundamental rule in refactoring is: **do not start without automated tests**. 63 | The space game already has tests in `test_space_game.py`. We will use the [pytest](https://pytest.org) library. 64 | Please make sure it is installed: 65 | 66 | pip install pytest 67 | 68 | You can run the tests from the `refactoring_tutorial/` folder: 69 | 70 | pytest test_space_game.py 71 | 72 | You should see a message like: 73 | 74 | ============================= test session starts ============================== 75 | platform linux -- Python 3.8.10, pytest-6.1.2, py-1.9.0, pluggy-0.13.1 76 | rootdir: /home/kristian/projects/refactoring_tutorial 77 | plugins: flake8-1.0.7, Faker-8.9.1, asyncio-0.15.1, cov-2.10.1, dash-1.18.1, anyio-3.5.0 78 | collected 12 items 79 | 80 | test_space_game.py ............ [100%] 81 | 82 | ============================== 12 passed in 0.04s ============================== 83 | 84 | To see the game output, do: 85 | 86 | pytest -s test_space_game.py::test_travel 87 | 88 | ---- 89 | 90 | ## 4. Identify problematic Code 91 | 92 | Now take a look at the main file `space_game.py`. 93 | Look for problematic sections that you would want to refactor. 94 | Note that the code has been linted (with [black](https://pypi.org/project/black/)). 95 | We are not looking for missing spaces or other style issues. 96 | 97 | Look for the following: 98 | 99 | - [ ] long Python modules 100 | - [ ] long functions that do not fit on a screen page 101 | - [ ] duplicate sections 102 | - [ ] code sections that are similar 103 | - [ ] code with many indentation levels 104 | - [ ] names of functions that are not descriptive 105 | - [ ] mixture of languages (e.g. HTML / SQL inside Python code) 106 | - [ ] code that mixes different domains together (e.g. user interface + business logic) 107 | - [ ] code that could be expressed more simply 108 | - [ ] code that you find hard to read 109 | 110 | Mark everything you find with a `#TODO` comment. 111 | 112 | ---- 113 | 114 | ## 5. Extract a Module 115 | 116 | Let's do our first refactoring. 117 | The first half of the code consists of a huge dictionary `TEXT`. 118 | Let's move that variable into a new Python file in the same folder. 119 | 120 | 1. create an empty Python file `text_en.py` 121 | 2. cut and paste the entire dictionary `TEXT` 122 | 3. add an import `from text_en import TEXT` 123 | 4. run the tests again 124 | 125 | The tests should still pass. 126 | 127 | This refactoring creates a separation of domains. 128 | Now it is a lot easier to e.g. add a second language. 129 | 130 | ---- 131 | 132 | ## 6. Extract Functions 133 | 134 | **The most fundamental refactoring technique is to split a long function into shorter ones.** 135 | 136 | We will make our toplevel function `travel()` easy to read. 137 | For that, we chop it into smaller pieces. 138 | By creating smaller functions, we either clean up the mess right away or at least create a smaller mess that is contained locally. 139 | 140 | We will use the following recipe: 141 | 142 | ### 6.1 Recipe: Extract a function 143 | 144 | This recipe has a few more steps: 145 | 146 | 1. Find a piece of code you want to move into a function 147 | 2. Give the function a name and create a `def` line 148 | 3. Move the code into the new function 149 | 4. Make a parameter out of every variable not created inside the function 150 | 5. Add a return statement at the end with every variable used later 151 | 6. Add a function call where you took the code 152 | 7. Run the tests 153 | 154 | Let's do this on a few examples: 155 | 156 | ### 6.2 Exercise: extract display_inventory 157 | 158 | The paragraph labeled **display inventory** on top of `travel()` makes a good refactoring candidate. 159 | Create a new function using the signature: 160 | 161 | def display_inventory(credits: bool, engines: bool, copilot: bool) -> None: 162 | 163 | This function does not need a return statement. 164 | 165 | Do not forget to run the tests afterwards. 166 | 167 | ### 6.3 Exercise: extract select_planet 168 | 169 | Extract a function `select_planet()` from the last code paragraph from the `travel()` function. 170 | 171 | This function needs a single parameter and a single return value. 172 | Find out what signature the function should have. 173 | 174 | Work through the recipe for extracting a function. 175 | 176 | ---- 177 | 178 | ## 7. Extract and Modify 179 | 180 | Sometimes, you need to modify a function to move it elsewhere. 181 | 182 | ### 7.1 Exercise: extract visit_planets 183 | 184 | To get a short and clean `travel()` function, it would be good to move the huge block with nested `if` statements out of the way. 185 | Let's extract a function `visit_planets()`. 186 | Start with the recipe for extracting a function. 187 | 188 | Use the signature: 189 | 190 | def visit_planet(planet: str, engines: bool, copilot: bool, credits: bool, game_end: bool) \ 191 | -> tuple[list[str], bool, bool, bool, bool]: 192 | ... 193 | 194 | and the function call: 195 | 196 | destinations, engines, copilot, credits, game_end = \ 197 | visit_planet(planet, engines, copilot, credits, game_end) 198 | 199 | **When you refactor the code, the tests should fail!** 200 | 201 | ### 7.2 The function does not work 202 | 203 | When you follow the recipe for extracting functions, the tests break. 204 | Something does not quite fit. 205 | The code block contains an extra `return` statement (in the black hole section). 206 | 207 | We need to modify two things to keep the code working: 208 | 209 | 1. Replace the `return` statement by `game_end = True` 210 | 2. Move the line printing end credits into the conditional branch where your copilot saves you 211 | 212 | Then run the tests. They should pass now. 213 | 214 | ### 7.3 How many functions should you extract? 215 | 216 | In an ideal world, **each function does exactly one thing**. 217 | What does that mean? 218 | 219 | In his [Clean Code Lectures](https://www.youtube.com/watch?v=7EmboKQH8lM), Uncle Bob (Robert C. Martin) states: 220 | 221 | Q: When is a function doing exactly one thing? 222 | 223 | A: When you cannot make two functions out of it. 224 | 225 | Although this is generally a good idea, you do not have to decompose everything **right away**. 226 | Often there are other, more important refactorings to take care of. 227 | 228 | ---- 229 | 230 | ## 8. Extract Data Structures 231 | 232 | After extracting a module and functions, the `travel()` function became a lot shorter already. 233 | But there are still many things to improve. 234 | Let's focus on the data structures: 235 | 236 | ### 8.1 Exercise: Extract boolean flags 237 | 238 | The function signature of `visit_planet()` is not very pretty. 239 | It contains a long list of boolean arguments. 240 | This was less obvious before. 241 | Our refactoring has exposed a problem with the data structures (or lack thereof). 242 | Let's take a closer look: 243 | 244 | The game progress is controlled by the booleans: `copilot`, `credits`, `engine` and `game_end`. 245 | These booleans are passed around several times. 246 | This is a sign that they could be placed in one data structure. 247 | 248 | What Python data structure can we use to store the presence or absence of multiple items? 249 | 250 | Let's use a Python `set` that we call `flags`. 251 | We need to modify a lot of code. 252 | 253 | First, define all the flags you have as a literal type on top of the file: 254 | 255 | from typing import Literal 256 | 257 | FLAG = Literal["credits", "engine", "copilot", "game_end"] 258 | 259 | instead of setting multiple booleans to `False` in `travel()`, define an empty set. 260 | 261 | flags: set[FLAG] = set() 262 | 263 | To check a flag, we would use its name as a string. So the `while` condition in `travel()` would become: 264 | 265 | while not ("game_end" in flags): 266 | 267 | Now, we need to change the function `display_inventory()` as well: 268 | 269 | 1. replace the boolean arguments by a single argument `flags` 270 | 2. modify the function call accordingly 271 | 3. modify the function body to use the `in` operator when checking state, e.g. `if "credits" in flags:` 272 | 273 | We need to do the same with `visit_planet()` 274 | 275 | 1. replace the boolean arguments by a single argument `flags` 276 | 2. modify the function call accordingly 277 | 3. remove the booleans from the return values (the set is mutable). `visit_planet()` only returns `planet` and `destinations`. 278 | 4. remove the booleans from the assigned return in `travel()` as well 279 | 5. modify the function body to use the `in` operator when checking state, e.g. `if "credits" in flags:` 280 | 6. modify the function body of `visit_planet()`. Whenever one of the booleans is modified, add to the set, e.g. `flags.add("game_end")` 281 | 282 | Finally, run the tests again. The tests should pass. 283 | 284 | *Note that looking up things in the set uses string comparison. This is not very performant, of course, but in a text adventure I frankly don't care. 285 | The advantage that we could support the Literal with a type checker is more important. 286 | If performance becomes important, one could replace the strings by integers. 287 | Also, if you believe performance is important, how about writing a performance test for it first?* 288 | 289 | ### 8.2 Extract puzzle functions 290 | 291 | The `visit_planet()` function is still very long. 292 | Now is a good moment to decompose it further. 293 | Create a function for the hyperdrive shopping scene on Centauri. 294 | 295 | The code left in `visit_planet()` should look like this: 296 | 297 | if planet == "centauri": 298 | print(TEXT["CENTAURI_DESCRIPTION"]) 299 | destinations = ["earth", "orion"] 300 | buy_hyperdrive(flags) 301 | 302 | Do the same for the other puzzles: 303 | 304 | def star_quiz(flags): 305 | 306 | def hire_copilot(flags): 307 | 308 | def black_hole(flags): 309 | 310 | Now `visit_planet()` should approximately fit on your screen. 311 | 312 | ### 8.3 Exercise: Extract a dictionary 313 | 314 | The destinations can be placed in a data structure as well. 315 | With each planet in `visit_planet()` there is always a list of destinations returned. 316 | 317 | Let's use the following dictionary instead: 318 | 319 | STARMAP = { 320 | 'earth': ['centauri', 'sirius'], 321 | 'centauri': ['earth', 'orion'], 322 | 'sirius': ..., 323 | 'orion': ..., 324 | 'black_hole': ['sirius'], 325 | } 326 | 327 | 1. place the dictionary on top of the Python file 328 | 2. fill in the two missing positions 329 | 3. remove the individual definitions of `destinations` 330 | 4. instead, at the end of the `visit_planet()` function, look up the destinations with `return STARMAP[planet]` 331 | 5. run the tests 332 | 333 | The tests should pass. 334 | 335 | ---- 336 | 337 | ## 9. Extract a Class 338 | 339 | By now, the `visit_planet()` function has become a lot shorter. 340 | But there is still has a huge nested `if` block. 341 | Let's see what we can do. 342 | 343 | ### 9.1 Are more dictionaries a good idea? 344 | 345 | Should we maybe extract the descriptions of each planet into *another* dictionary? 346 | We would get: 347 | 348 | PLANET_DESCRIPTIONS = { 349 | 'earth': TEXT['EARTH_DESCRIPTION], 350 | 'sirius': TEXT['SIRIUS_DESCRIPTION], 351 | ... 352 | } 353 | 354 | You could do this, and it would further simplify `visit_planet()`. 355 | But seeing multiple dictionaries with the same keys is a clear hint that there is a deeper structure in our code. 356 | We will extract a class. 357 | 358 | ### 9.2 Exercise: The Planet class 359 | 360 | We find a couple of things that the planets have in common: 361 | 362 | * every planet has a name 363 | * every planet has a description 364 | * every planet has connections to other planets 365 | 366 | These are attributes of the new class. 367 | 368 | Let's define a new class with the following signature: 369 | 370 | from pydantic import BaseModel 371 | 372 | class Planet(BaseModel): 373 | 374 | name: str 375 | description: str 376 | connections: list[str] 377 | 378 | Run the tests to make sure you didn't mess up anything (even though we do not use the class yet). 379 | 380 | ### 9.3 Exercise: Add a method 381 | 382 | We will convert the function `visit_planet()` into a method of the new `Planet` class. 383 | 384 | Move the entire code from `visit_planet()` into a new method with the signature: 385 | 386 | def visit(self, flags: set[FLAG]) -> list[str]: 387 | 388 | As the first thing, have the planet print its own description: 389 | 390 | print(self.description) 391 | 392 | That removes a few lines from the function and makes the code easier to read. 393 | 394 | The tests won't pass at this point. You may want to run them to make sure you are editing the right file. 395 | 396 | 397 | ### 9.4 Exercise: Create instances 398 | 399 | Let's create a dictionary of planets. 400 | We will do so on the module level, replacing `STARMAP`: 401 | 402 | PLANETS = { 403 | 'earth': Planet( 404 | name='earth', 405 | description=TEXT['EARTH_DESCRIPTION'], 406 | connections=['centauri', 'sirius'] 407 | ), 408 | ... 409 | } 410 | 411 | We use the `Planet` instances in the `travel()` function. 412 | The code should be 413 | 414 | planet = PLANETS['earth'] 415 | ... 416 | while ...: 417 | planet.visit(flags) 418 | display_destinations(planet) 419 | planet = select_planet(planet.connections) 420 | 421 | Note that you need to modify these methods slightly. 422 | 423 | At this point, the tests should pass. 424 | 425 | ### 9.5 Exercise: Breaking down the visit function 426 | 427 | Finally, we have restructured our code to a point where we can decompose the huge block of `if` statements. 428 | 429 | Some planets have a puzzle. Add a puzzle attribute to `Planet.__init__()` 430 | 431 | Next, we pass these functions as callbacks in the `puzzle` argument when creating `Planet` objects. 432 | One entry in the `PLANETS` dict would look like: 433 | 434 | 'sirius`: Planet('sirius', TEXT['SIRIUS_DESCRIPTION'], star_quiz) 435 | 436 | Now in the `visit()` method, all you need to do is call the callback: 437 | 438 | if puzzle: 439 | puzzle(flags) 440 | 441 | And the multiple `if` statements should evaporate. 442 | 443 | ---- 444 | 445 | ## 10. Other Refactoring Strategies 446 | 447 | ### 10.1 Names matter 448 | 449 | *"Planet"* is not an accurate name from an astronomic point of view. 450 | On the other hand, I would refuse to call anything *"System"* on a computer, because it may mean anything. 451 | 452 | From a game design point of view, *"Room"* or *"Location"* could be better. These are good questions to discuss with the domain experts and colleagues on your team. Finding common vocabulary is one good side effect successful refactoring may have. 453 | 454 | ### 10.2 Programming paradigms 455 | 456 | When refactoring Python code, you often have multiple options. 457 | It helps if you have a **programming paradigm** in mind that you are working towards, such as: 458 | 459 | * functional programming with stateless functions that can be recombined 460 | * strictly object-oriented programming 461 | * hybrid architecture with core classes and toplevel functions 462 | * look for specific **Design Patterns** that describe well what your code is doing 463 | * practice TDD and write additional tests when extracting larger units of code 464 | 465 | In my experience, refactoring is much about executing a few standard techniques consistently. 466 | 467 | You find a great list of refactoring techniques on [refactoring.guru](https://refactoring.guru/) by Alexander Shvets. 468 | 469 | ### 10.3 Embrace future change 470 | 471 | In refactoring, you always want to separate things that are likely to change from things that don't. 472 | What might change in a text adventure? 473 | 474 | * connections between planets 475 | * puzzles on the planets 476 | * new planets 477 | * almost any text 478 | * a graphical or web interface (replacing the `print()` statements would justify a complete rewrite in this case) 479 | 480 | With well-refactored code, any of the above should require changing a single location in the code. 481 | 482 | In the end, our rectorings should make it easy to add more planets, puzzles or write a completely new adventure. 483 | 484 | **Give it a try and have fun programming!** 485 | 486 | ---- 487 | 488 | ## 11. Closing Remarks 489 | 490 | Refactoring is like washing. It is most effective if repeated regularly. 491 | 492 | Of course, one could wait for two weeks, so that taking a shower is really worth it. 493 | But in practice this is not such a good idea, at least not if you are working with other people. 494 | 495 | It is the same with refactoring. 496 | 497 | ---- 498 | 499 | ## License 500 | 501 | (c) 2022 Dr. Kristian Rother `kristian.rother@posteo.de` 502 | 503 | This tutorial is subject to the MIT License. Have fun sharing! 504 | 505 | See LICENSE for details. -------------------------------------------------------------------------------- /advanced/05-extract-module/space_game.py: -------------------------------------------------------------------------------- 1 | """ 2 | Space Travel Game 3 | 4 | A simple text adventure written for a refactoring tutorial. 5 | """ 6 | from text_en import TEXT 7 | 8 | 9 | def travel(): 10 | 11 | print(TEXT["OPENING_MESSAGE"]) 12 | 13 | planet = "earth" 14 | engines = False 15 | copilot = False 16 | credits = False 17 | crystal_found = False 18 | 19 | while not crystal_found: 20 | 21 | print(TEXT["BAR"]) 22 | 23 | # display inventory 24 | if credits: 25 | print("You have plenty of stellar credits.") 26 | if engines: 27 | print("You have a brand new next-gen hyperdrive.") 28 | if copilot: 29 | print("A furry tech-savvy copilot is on board.") 30 | 31 | # 32 | # interaction with planets 33 | # 34 | if planet == "earth": 35 | destinations = ["centauri", "sirius"] 36 | print(TEXT["EARTH_DESCRIPTION"]) 37 | 38 | if planet == "centauri": 39 | print(TEXT["CENTAURI_DESCRIPTION"]) 40 | destinations = ["earth", "orion"] 41 | 42 | if not engines: 43 | print(TEXT["HYPERDRIVE_SHOPPING_QUESTION"]) 44 | if input() == "yes": 45 | if credits: 46 | print(TEXT["HYPERDRIVE_SHOPPING_SUCCESS"]) 47 | engines = True 48 | else: 49 | print(TEXT["HYPERDRIVE_TOO_EXPENSIVE"]) 50 | 51 | if planet == "sirius": 52 | print(TEXT["SIRIUS_DESCRIPTION"]) 53 | destinations = ["orion", "earth", "BH#0997"] 54 | 55 | if not credits: 56 | print(TEXT["SIRIUS_QUIZ_QUESTION"]) 57 | answer = input() 58 | if answer == "2": 59 | print(TEXT["SIRIUS_QUIZ_CORRECT"]) 60 | credits = True 61 | else: 62 | print(TEXT["SIRIUS_QUIZ_INCORRECT"]) 63 | 64 | if planet == "orion": 65 | destinations = ["centauri", "sirius"] 66 | 67 | if engines and not copilot: 68 | print(TEXT["ORION_DESCRIPTION"]) 69 | print(TEXT["ORION_HIRE_COPILOT_QUESTION"]) 70 | if input() == "yes": 71 | copilot = True 72 | else: 73 | print(TEXT["ORION_DESCRIPTION"]) 74 | print(TEXT["ORION_NOTHING_GOING_ON"]) 75 | 76 | if planet == "BH#0997": 77 | print(TEXT["BLACK_HOLE_DESCRIPTION"]) 78 | destinations = ["sirius"] 79 | if input(TEXT["BLACK_HOLE_EXAMINE_QUESTION"]) == "yes": 80 | if engines and copilot: 81 | print(TEXT["BLACK_HOLE_COPILOT_SAVES_YOU"]) 82 | planet = "oracle" 83 | else: 84 | print(TEXT["BLACK_HOLE_CRUNCHED"]) 85 | return 86 | 87 | if planet == "oracle": 88 | print(TEXT["ORACLE_QUESTION"]) 89 | answer = input() 90 | if answer == "42": 91 | print(TEXT["ORACLE_CORRECT"]) 92 | crystal_found = True 93 | else: 94 | print(TEXT["ORACLE_INCORRECT"]) 95 | engines = False 96 | 97 | destinations = ["earth"] 98 | 99 | # display hyperjump destinations 100 | print("\nWhere do you want to travel?") 101 | position = 1 102 | for d in destinations: 103 | print(f"[{position}] {d}") 104 | position += 1 105 | 106 | # choose the next planet 107 | travel_to = None 108 | while travel_to not in destinations: 109 | text = input() 110 | try: 111 | index = int(text) 112 | travel_to = destinations[index - 1] 113 | except ValueError: 114 | print("please enter a number") 115 | except IndexError: 116 | print(f"please enter 1-{len(destinations)}") 117 | planet = travel_to 118 | 119 | print(TEXT["END_CREDITS"]) 120 | 121 | 122 | if __name__ == "__main__": 123 | travel() 124 | -------------------------------------------------------------------------------- /advanced/05-extract-module/test_space_game.py: -------------------------------------------------------------------------------- 1 | import io 2 | import pytest 3 | 4 | from space_game import travel 5 | 6 | 7 | # the actual solution to the game 8 | SOLUTION = [ 9 | "2", 10 | "2", # go to sirius and win quiz 11 | "1", 12 | "1", 13 | "yes", # go to centauri and buy GPU drive 14 | "2", 15 | "yes", # hire copilot on orion 16 | "2", 17 | "3", 18 | "yes", # jump into black hole 19 | "42", 20 | "1", # get crystal 21 | ] 22 | 23 | DIE_BY_BLACK_HOLE = [ 24 | "2", 25 | "2", # go to sirius and win quiz 26 | "1", 27 | "1", 28 | "yes", # go to centauri and buy GPU drive 29 | "2", 30 | "no", # hire copilot on orion 31 | "2", 32 | "3", 33 | "yes", # jump into black hole 34 | ] 35 | 36 | # text sniplets that should appear literally in the output 37 | PHRASES = [ 38 | "The stars are waiting for you", 39 | "Betelgeuse", 40 | "credits", 41 | "tech-savvy copilot", 42 | "buy", 43 | "Do you want to hire them", 44 | "Black Hole", 45 | "stupid idea", 46 | "Return to your people in peace", 47 | "THE END", 48 | ] 49 | 50 | 51 | @pytest.fixture 52 | def solution_input(): 53 | """helper function to hijack the keyboard for testing""" 54 | return io.StringIO("\n".join(SOLUTION)) 55 | 56 | 57 | def test_travel(monkeypatch, solution_input): 58 | """game finishes""" 59 | monkeypatch.setattr("sys.stdin", solution_input) 60 | travel() 61 | 62 | 63 | def test_output(monkeypatch, capsys, solution_input): 64 | """text output is not empty""" 65 | monkeypatch.setattr("sys.stdin", solution_input) 66 | 67 | travel() 68 | 69 | captured = capsys.readouterr() 70 | assert len(captured.out) > 0 71 | 72 | 73 | def test_die(monkeypatch, capsys): 74 | """player dies""" 75 | monkeypatch.setattr("sys.stdin", io.StringIO("\n".join(DIE_BY_BLACK_HOLE))) 76 | 77 | travel() 78 | 79 | captured = capsys.readouterr() 80 | assert "crunches" in captured.out 81 | 82 | 83 | @pytest.mark.parametrize("phrase", PHRASES) 84 | def test_output_phrases(monkeypatch, capsys, solution_input, phrase): 85 | """check for some key phrases in the output""" 86 | monkeypatch.setattr("sys.stdin", solution_input) 87 | 88 | travel() 89 | 90 | captured = capsys.readouterr() 91 | assert phrase in captured.out 92 | -------------------------------------------------------------------------------- /advanced/05-extract-module/text_en.py: -------------------------------------------------------------------------------- 1 | TEXT = { 2 | "OPENING_MESSAGE": """ 3 | ********************************************************************* 4 | 5 | You and your trusted spaceship set out to look for 6 | fame, wisdom, and adventure. 7 | The stars are waiting for you. 8 | 9 | """, 10 | "BAR": "\n*********************************************************************\n", 11 | "EARTH_DESCRIPTION": "\nYou are on Earth. The cradle of humankind. A rather dull place.", 12 | "CENTAURI_DESCRIPTION": "\nYou are on Alpha Centauri. A buzzing trade hub.\nYou can buy almost anything here.", 13 | "HYPERDRIVE_SHOPPING_QUESTION": """ 14 | They have a brand new next-generation hyperdrive with a built-in GPU. 15 | 16 | Would you like to buy one [yes/no]""", 17 | "HYPERDRIVE_TOO_EXPENSIVE": """ 18 | You cannot afford it. The GPU is too expensive.""", 19 | "HYPERDRIVE_SHOPPING_SUCCESS": """ 20 | Your spaceship now has a shiny hyperdrive on both sides. 21 | """, 22 | "SIRIUS_DESCRIPTION": """ 23 | You are on Sirius. The system is full of media companies 24 | and content delivery networks. 25 | """, 26 | "SIRIUS_QUIZ_QUESTION": """ 27 | You manage to get a place in *Stellar* - the greatest quiz show in the universe. 28 | And here is your question: 29 | 30 | Which star do you find on the shoulder of Orion? 31 | 32 | [1] Altair 33 | [2] Betelgeuse 34 | [3] Aldebaran 35 | [4] Andromeda 36 | """, 37 | "SIRIUS_QUIZ_CORRECT": """ 38 | *Correct!!!* You win a ton or credits. 39 | """, 40 | "SIRIUS_QUIZ_INCORRECT": """ 41 | Sorry, this was the wrong answer. Don't take it too sirius. 42 | Better luck next time. 43 | """, 44 | "ORION_DESCRIPTION": """ 45 | You are on Orion. An icy world inhabited by monosyllabic furry sentients. 46 | """, 47 | "ORION_HIRE_COPILOT_QUESTION": """ 48 | You approach the natives at the spaceport 49 | One of them points at your hyperdrive and says 'Nice!' 50 | 51 | Do you want to hire them as a copilot? [yes/no] 52 | """, 53 | "ORION_NOTHING_GOING_ON": """ 54 | They are not very talkative. 55 | """, 56 | "BLACK_HOLE_DESCRIPTION": """ 57 | You are close to Black Hole #0997. Maybe coming here was a really stupid idea. 58 | """, 59 | "BLACK_HOLE_EXAMINE_QUESTION": """ 60 | Do you want to examine the black hole closer? [yes/no] 61 | """, 62 | "BLACK_HOLE_CRUNCHED": """ 63 | The black hole crunches you into a tiny piece of dust. 64 | 65 | THE END 66 | """, 67 | "BLACK_HOLE_COPILOT_SAVES_YOU": """ 68 | On the rim of the black hole your copilot blurts out: 69 | 70 | Turn left! 71 | 72 | You ignite the next-gen hyperdrive, creating a time-space anomaly. 73 | You are transported to an orthogonal dimension. 74 | """, 75 | "ORACLE_QUESTION": """ 76 | You have reached the cosmic oracle. With a booming telepathic voice it proclaims: 77 | 78 | What is the answer to life, the universe and everything? 79 | 80 | What do you reply? 81 | """, 82 | "ORACLE_CORRECT": """ 83 | You are worthy of our knowledge. In this crystal you will find a matter conversion tutorial. 84 | Return to your people in peace. 85 | 86 | A portal opens in front of you. 87 | """, 88 | "ORACLE_INCORRECT": """ 89 | You have much to learn still. Return to your people in peace. But we will keep that funny toy of yours. 90 | 91 | A portal opens in front of you. 92 | """, 93 | "END_CREDITS": """ 94 | You return from your adventure wise and famous. 95 | You have found the saged and fabulous crystal. Once decyphered, 96 | it will advance the knowledge of all sentient beings by generations. 97 | 98 | It may also look good in your cockpit. 99 | 100 | THE END 101 | """, 102 | } 103 | -------------------------------------------------------------------------------- /advanced/06-extract-function/space_game.py: -------------------------------------------------------------------------------- 1 | """ 2 | Space Travel Game 3 | 4 | A simple text adventure written for a refactoring tutorial. 5 | """ 6 | from text_en import TEXT 7 | 8 | 9 | def display_inventory(credits, engines, copilot): 10 | print(TEXT["BAR"]) 11 | if credits: 12 | print("You have plenty of stellar credits.") 13 | if engines: 14 | print("You have a brand new next-gen hyperdrive.") 15 | if copilot: 16 | print("A furry tech-savvy copilot is on board.") 17 | 18 | 19 | def display_destinations(destinations): 20 | print("\nWhere do you want to travel?") 21 | position = 1 22 | for d in destinations: 23 | print(f"[{position}] {d}") 24 | position += 1 25 | 26 | 27 | def select_planet(destinations): 28 | # choose the next planet 29 | travel_to = None 30 | while travel_to not in destinations: 31 | text = input() 32 | try: 33 | index = int(text) 34 | travel_to = destinations[index - 1] 35 | except ValueError: 36 | print("please enter a number") 37 | except IndexError: 38 | print(f"please enter 1-{len(destinations)}") 39 | return travel_to 40 | 41 | 42 | def travel(): 43 | 44 | print(TEXT["OPENING_MESSAGE"]) 45 | 46 | planet = "earth" 47 | engines = False 48 | copilot = False 49 | credits = False 50 | crystal_found = False 51 | 52 | while not crystal_found: 53 | 54 | display_inventory(credits, engines, copilot) 55 | 56 | # 57 | # interaction with planets 58 | # 59 | if planet == "earth": 60 | destinations = ["centauri", "sirius"] 61 | print(TEXT["EARTH_DESCRIPTION"]) 62 | 63 | if planet == "centauri": 64 | print(TEXT["CENTAURI_DESCRIPTION"]) 65 | destinations = ["earth", "orion"] 66 | 67 | if not engines: 68 | print(TEXT["HYPERDRIVE_SHOPPING_QUESTION"]) 69 | if input() == "yes": 70 | if credits: 71 | print(TEXT["HYPERDRIVE_SHOPPING_SUCCESS"]) 72 | engines = True 73 | else: 74 | print(TEXT["HYPERDRIVE_TOO_EXPENSIVE"]) 75 | 76 | if planet == "sirius": 77 | print(TEXT["SIRIUS_DESCRIPTION"]) 78 | destinations = ["orion", "earth", "BH#0997"] 79 | 80 | if not credits: 81 | print(TEXT["SIRIUS_QUIZ_QUESTION"]) 82 | answer = input() 83 | if answer == "2": 84 | print(TEXT["SIRIUS_QUIZ_CORRECT"]) 85 | credits = True 86 | else: 87 | print(TEXT["SIRIUS_QUIZ_INCORRECT"]) 88 | 89 | if planet == "orion": 90 | destinations = ["centauri", "sirius"] 91 | 92 | if engines and not copilot: 93 | print(TEXT["ORION_DESCRIPTION"]) 94 | print(TEXT["ORION_HIRE_COPILOT_QUESTION"]) 95 | if input() == "yes": 96 | copilot = True 97 | else: 98 | print(TEXT["ORION_DESCRIPTION"]) 99 | print(TEXT["ORION_NOTHING_GOING_ON"]) 100 | 101 | if planet == "BH#0997": 102 | print(TEXT["BLACK_HOLE_DESCRIPTION"]) 103 | destinations = ["sirius"] 104 | if input(TEXT["BLACK_HOLE_EXAMINE_QUESTION"]) == "yes": 105 | if engines and copilot: 106 | print(TEXT["BLACK_HOLE_COPILOT_SAVES_YOU"]) 107 | planet = "oracle" 108 | else: 109 | print(TEXT["BLACK_HOLE_CRUNCHED"]) 110 | return 111 | 112 | if planet == "oracle": 113 | print(TEXT["ORACLE_QUESTION"]) 114 | answer = input() 115 | if answer == "42": 116 | print(TEXT["ORACLE_CORRECT"]) 117 | crystal_found = True 118 | else: 119 | print(TEXT["ORACLE_INCORRECT"]) 120 | engines = False 121 | 122 | destinations = ["earth"] 123 | 124 | display_destinations(destinations) 125 | planet = select_planet(destinations) 126 | 127 | print(TEXT["END_CREDITS"]) 128 | 129 | 130 | if __name__ == "__main__": 131 | travel() 132 | -------------------------------------------------------------------------------- /advanced/06-extract-function/test_space_game.py: -------------------------------------------------------------------------------- 1 | import io 2 | import pytest 3 | 4 | from space_game import travel 5 | 6 | 7 | # the actual solution to the game 8 | SOLUTION = [ 9 | "2", 10 | "2", # go to sirius and win quiz 11 | "1", 12 | "1", 13 | "yes", # go to centauri and buy GPU drive 14 | "2", 15 | "yes", # hire copilot on orion 16 | "2", 17 | "3", 18 | "yes", # jump into black hole 19 | "42", 20 | "1", # get crystal 21 | ] 22 | 23 | DIE_BY_BLACK_HOLE = [ 24 | "2", 25 | "2", # go to sirius and win quiz 26 | "1", 27 | "1", 28 | "yes", # go to centauri and buy GPU drive 29 | "2", 30 | "no", # hire copilot on orion 31 | "2", 32 | "3", 33 | "yes", # jump into black hole 34 | ] 35 | 36 | # text sniplets that should appear literally in the output 37 | PHRASES = [ 38 | "The stars are waiting for you", 39 | "Betelgeuse", 40 | "credits", 41 | "tech-savvy copilot", 42 | "buy", 43 | "Do you want to hire them", 44 | "Black Hole", 45 | "stupid idea", 46 | "Return to your people in peace", 47 | "THE END", 48 | ] 49 | 50 | 51 | @pytest.fixture 52 | def solution_input(): 53 | """helper function to hijack the keyboard for testing""" 54 | return io.StringIO("\n".join(SOLUTION)) 55 | 56 | 57 | def test_travel(monkeypatch, solution_input): 58 | """game finishes""" 59 | monkeypatch.setattr("sys.stdin", solution_input) 60 | travel() 61 | 62 | 63 | def test_output(monkeypatch, capsys, solution_input): 64 | """text output is not empty""" 65 | monkeypatch.setattr("sys.stdin", solution_input) 66 | 67 | travel() 68 | 69 | captured = capsys.readouterr() 70 | assert len(captured.out) > 0 71 | 72 | 73 | def test_die(monkeypatch, capsys): 74 | """player dies""" 75 | monkeypatch.setattr("sys.stdin", io.StringIO("\n".join(DIE_BY_BLACK_HOLE))) 76 | 77 | travel() 78 | 79 | captured = capsys.readouterr() 80 | assert "crunches" in captured.out 81 | 82 | 83 | @pytest.mark.parametrize("phrase", PHRASES) 84 | def test_output_phrases(monkeypatch, capsys, solution_input, phrase): 85 | """check for some key phrases in the output""" 86 | monkeypatch.setattr("sys.stdin", solution_input) 87 | 88 | travel() 89 | 90 | captured = capsys.readouterr() 91 | assert phrase in captured.out 92 | -------------------------------------------------------------------------------- /advanced/06-extract-function/text_en.py: -------------------------------------------------------------------------------- 1 | TEXT = { 2 | "OPENING_MESSAGE": """ 3 | ********************************************************************* 4 | 5 | You and your trusted spaceship set out to look for 6 | fame, wisdom, and adventure. 7 | The stars are waiting for you. 8 | 9 | """, 10 | "BAR": "\n*********************************************************************\n", 11 | "EARTH_DESCRIPTION": "\nYou are on Earth. The cradle of humankind. A rather dull place.", 12 | "CENTAURI_DESCRIPTION": "\nYou are on Alpha Centauri. A buzzing trade hub.\nYou can buy almost anything here.", 13 | "HYPERDRIVE_SHOPPING_QUESTION": """ 14 | They have a brand new next-generation hyperdrive with a built-in GPU. 15 | 16 | Would you like to buy one [yes/no]""", 17 | "HYPERDRIVE_TOO_EXPENSIVE": """ 18 | You cannot afford it. The GPU is too expensive.""", 19 | "HYPERDRIVE_SHOPPING_SUCCESS": """ 20 | Your spaceship now has a shiny hyperdrive on both sides. 21 | """, 22 | "SIRIUS_DESCRIPTION": """ 23 | You are on Sirius. The system is full of media companies 24 | and content delivery networks. 25 | """, 26 | "SIRIUS_QUIZ_QUESTION": """ 27 | You manage to get a place in *Stellar* - the greatest quiz show in the universe. 28 | And here is your question: 29 | 30 | Which star do you find on the shoulder of Orion? 31 | 32 | [1] Altair 33 | [2] Betelgeuse 34 | [3] Aldebaran 35 | [4] Andromeda 36 | """, 37 | "SIRIUS_QUIZ_CORRECT": """ 38 | *Correct!!!* You win a ton or credits. 39 | """, 40 | "SIRIUS_QUIZ_INCORRECT": """ 41 | Sorry, this was the wrong answer. Don't take it too sirius. 42 | Better luck next time. 43 | """, 44 | "ORION_DESCRIPTION": """ 45 | You are on Orion. An icy world inhabited by monosyllabic furry sentients. 46 | """, 47 | "ORION_HIRE_COPILOT_QUESTION": """ 48 | You approach the natives at the spaceport 49 | One of them points at your hyperdrive and says 'Nice!' 50 | 51 | Do you want to hire them as a copilot? [yes/no] 52 | """, 53 | "ORION_NOTHING_GOING_ON": """ 54 | They are not very talkative. 55 | """, 56 | "BLACK_HOLE_DESCRIPTION": """ 57 | You are close to Black Hole #0997. Maybe coming here was a really stupid idea. 58 | """, 59 | "BLACK_HOLE_EXAMINE_QUESTION": """ 60 | Do you want to examine the black hole closer? [yes/no] 61 | """, 62 | "BLACK_HOLE_CRUNCHED": """ 63 | The black hole crunches you into a tiny piece of dust. 64 | 65 | THE END 66 | """, 67 | "BLACK_HOLE_COPILOT_SAVES_YOU": """ 68 | On the rim of the black hole your copilot blurts out: 69 | 70 | Turn left! 71 | 72 | You ignite the next-gen hyperdrive, creating a time-space anomaly. 73 | You are transported to an orthogonal dimension. 74 | """, 75 | "ORACLE_QUESTION": """ 76 | You have reached the cosmic oracle. With a booming telepathic voice it proclaims: 77 | 78 | What is the answer to life, the universe and everything? 79 | 80 | What do you reply? 81 | """, 82 | "ORACLE_CORRECT": """ 83 | You are worthy of our knowledge. In this crystal you will find a matter conversion tutorial. 84 | Return to your people in peace. 85 | 86 | A portal opens in front of you. 87 | """, 88 | "ORACLE_INCORRECT": """ 89 | You have much to learn still. Return to your people in peace. But we will keep that funny toy of yours. 90 | 91 | A portal opens in front of you. 92 | """, 93 | "END_CREDITS": """ 94 | You return from your adventure wise and famous. 95 | You have found the saged and fabulous crystal. Once decyphered, 96 | it will advance the knowledge of all sentient beings by generations. 97 | 98 | It may also look good in your cockpit. 99 | 100 | THE END 101 | """, 102 | } 103 | -------------------------------------------------------------------------------- /advanced/07-extract-and-modify/space_game.py: -------------------------------------------------------------------------------- 1 | """ 2 | Space Travel Game 3 | 4 | A simple text adventure written for a refactoring tutorial. 5 | """ 6 | from text_en import TEXT 7 | 8 | 9 | def display_inventory(credits, engines, copilot): 10 | print(TEXT["BAR"]) 11 | if credits: 12 | print("You have plenty of stellar credits.") 13 | if engines: 14 | print("You have a brand new next-gen hyperdrive.") 15 | if copilot: 16 | print("A furry tech-savvy copilot is on board.") 17 | 18 | 19 | def display_destinations(destinations): 20 | print("\nWhere do you want to travel?") 21 | position = 1 22 | for d in destinations: 23 | print(f"[{position}] {d}") 24 | position += 1 25 | 26 | 27 | def select_planet(destinations): 28 | # choose the next planet 29 | travel_to = None 30 | while travel_to not in destinations: 31 | text = input() 32 | try: 33 | index = int(text) 34 | travel_to = destinations[index - 1] 35 | except ValueError: 36 | print("please enter a number") 37 | except IndexError: 38 | print(f"please enter 1-{len(destinations)}") 39 | return travel_to 40 | 41 | 42 | def visit_planet(planet, engines, copilot, credits, crystal_found, dead): 43 | 44 | if planet == "earth": 45 | destinations = ["centauri", "sirius"] 46 | print(TEXT["EARTH_DESCRIPTION"]) 47 | 48 | if planet == "centauri": 49 | print(TEXT["CENTAURI_DESCRIPTION"]) 50 | destinations = ["earth", "orion"] 51 | 52 | if not engines: 53 | print(TEXT["HYPERDRIVE_SHOPPING_QUESTION"]) 54 | if input() == "yes": 55 | if credits: 56 | print(TEXT["HYPERDRIVE_SHOPPING_SUCCESS"]) 57 | engines = True 58 | else: 59 | print(TEXT["HYPERDRIVE_TOO_EXPENSIVE"]) 60 | 61 | if planet == "sirius": 62 | print(TEXT["SIRIUS_DESCRIPTION"]) 63 | destinations = ["orion", "earth", "BH#0997"] 64 | 65 | if not credits: 66 | print(TEXT["SIRIUS_QUIZ_QUESTION"]) 67 | answer = input() 68 | if answer == "2": 69 | print(TEXT["SIRIUS_QUIZ_CORRECT"]) 70 | credits = True 71 | else: 72 | print(TEXT["SIRIUS_QUIZ_INCORRECT"]) 73 | 74 | if planet == "orion": 75 | destinations = ["centauri", "sirius"] 76 | 77 | if engines and not copilot: 78 | print(TEXT["ORION_DESCRIPTION"]) 79 | print(TEXT["ORION_HIRE_COPILOT_QUESTION"]) 80 | if input() == "yes": 81 | copilot = True 82 | else: 83 | print(TEXT["ORION_DESCRIPTION"]) 84 | print(TEXT["ORION_NOTHING_GOING_ON"]) 85 | 86 | if planet == "BH#0997": 87 | print(TEXT["BLACK_HOLE_DESCRIPTION"]) 88 | destinations = ["sirius"] 89 | if input(TEXT["BLACK_HOLE_EXAMINE_QUESTION"]) == "yes": 90 | if engines and copilot: 91 | print(TEXT["BLACK_HOLE_COPILOT_SAVES_YOU"]) 92 | planet = "oracle" 93 | else: 94 | print(TEXT["BLACK_HOLE_CRUNCHED"]) 95 | dead = True 96 | 97 | if planet == "oracle": 98 | print(TEXT["ORACLE_QUESTION"]) 99 | answer = input() 100 | if answer == "42": 101 | print(TEXT["ORACLE_CORRECT"]) 102 | crystal_found = True 103 | else: 104 | print(TEXT["ORACLE_INCORRECT"]) 105 | engines = False 106 | 107 | destinations = ["earth"] 108 | 109 | return planet, destinations, credits, engines, copilot, crystal_found, dead 110 | 111 | 112 | def travel(): 113 | 114 | print(TEXT["OPENING_MESSAGE"]) 115 | 116 | planet = "earth" 117 | engines = False 118 | copilot = False 119 | credits = False 120 | crystal_found = False 121 | dead = False 122 | 123 | while not crystal_found and not dead: 124 | 125 | display_inventory(credits, engines, copilot) 126 | 127 | planet, destinations, credits, engines, copilot, crystal_found, dead = \ 128 | visit_planet(planet, engines, copilot, credits, crystal_found, dead) 129 | 130 | if not dead: 131 | display_destinations(destinations) 132 | planet = select_planet(destinations) 133 | 134 | print(TEXT["END_CREDITS"]) 135 | 136 | 137 | if __name__ == "__main__": 138 | travel() 139 | -------------------------------------------------------------------------------- /advanced/07-extract-and-modify/test_space_game.py: -------------------------------------------------------------------------------- 1 | import io 2 | import pytest 3 | 4 | from space_game import travel 5 | 6 | 7 | # the actual solution to the game 8 | SOLUTION = [ 9 | "2", 10 | "2", # go to sirius and win quiz 11 | "1", 12 | "1", 13 | "yes", # go to centauri and buy GPU drive 14 | "2", 15 | "yes", # hire copilot on orion 16 | "2", 17 | "3", 18 | "yes", # jump into black hole 19 | "42", 20 | "1", # get crystal 21 | ] 22 | 23 | DIE_BY_BLACK_HOLE = [ 24 | "2", 25 | "2", # go to sirius and win quiz 26 | "1", 27 | "1", 28 | "yes", # go to centauri and buy GPU drive 29 | "2", 30 | "no", # hire copilot on orion 31 | "2", 32 | "3", 33 | "yes", # jump into black hole 34 | ] 35 | 36 | # text sniplets that should appear literally in the output 37 | PHRASES = [ 38 | "The stars are waiting for you", 39 | "Betelgeuse", 40 | "credits", 41 | "tech-savvy copilot", 42 | "buy", 43 | "Do you want to hire them", 44 | "Black Hole", 45 | "stupid idea", 46 | "Return to your people in peace", 47 | "THE END", 48 | ] 49 | 50 | 51 | @pytest.fixture 52 | def solution_input(): 53 | """helper function to hijack the keyboard for testing""" 54 | return io.StringIO("\n".join(SOLUTION)) 55 | 56 | 57 | def test_travel(monkeypatch, solution_input): 58 | """game finishes""" 59 | monkeypatch.setattr("sys.stdin", solution_input) 60 | travel() 61 | 62 | 63 | def test_output(monkeypatch, capsys, solution_input): 64 | """text output is not empty""" 65 | monkeypatch.setattr("sys.stdin", solution_input) 66 | 67 | travel() 68 | 69 | captured = capsys.readouterr() 70 | assert len(captured.out) > 0 71 | 72 | 73 | def test_die(monkeypatch, capsys): 74 | """player dies""" 75 | monkeypatch.setattr("sys.stdin", io.StringIO("\n".join(DIE_BY_BLACK_HOLE))) 76 | 77 | travel() 78 | 79 | captured = capsys.readouterr() 80 | assert "crunches" in captured.out 81 | 82 | 83 | @pytest.mark.parametrize("phrase", PHRASES) 84 | def test_output_phrases(monkeypatch, capsys, solution_input, phrase): 85 | """check for some key phrases in the output""" 86 | monkeypatch.setattr("sys.stdin", solution_input) 87 | 88 | travel() 89 | 90 | captured = capsys.readouterr() 91 | assert phrase in captured.out 92 | -------------------------------------------------------------------------------- /advanced/07-extract-and-modify/text_en.py: -------------------------------------------------------------------------------- 1 | TEXT = { 2 | "OPENING_MESSAGE": """ 3 | ********************************************************************* 4 | 5 | You and your trusted spaceship set out to look for 6 | fame, wisdom, and adventure. 7 | The stars are waiting for you. 8 | 9 | """, 10 | "BAR": "\n*********************************************************************\n", 11 | "EARTH_DESCRIPTION": "\nYou are on Earth. The cradle of humankind. A rather dull place.", 12 | "CENTAURI_DESCRIPTION": "\nYou are on Alpha Centauri. A buzzing trade hub.\nYou can buy almost anything here.", 13 | "HYPERDRIVE_SHOPPING_QUESTION": """ 14 | They have a brand new next-generation hyperdrive with a built-in GPU. 15 | 16 | Would you like to buy one [yes/no]""", 17 | "HYPERDRIVE_TOO_EXPENSIVE": """ 18 | You cannot afford it. The GPU is too expensive.""", 19 | "HYPERDRIVE_SHOPPING_SUCCESS": """ 20 | Your spaceship now has a shiny hyperdrive on both sides. 21 | """, 22 | "SIRIUS_DESCRIPTION": """ 23 | You are on Sirius. The system is full of media companies 24 | and content delivery networks. 25 | """, 26 | "SIRIUS_QUIZ_QUESTION": """ 27 | You manage to get a place in *Stellar* - the greatest quiz show in the universe. 28 | And here is your question: 29 | 30 | Which star do you find on the shoulder of Orion? 31 | 32 | [1] Altair 33 | [2] Betelgeuse 34 | [3] Aldebaran 35 | [4] Andromeda 36 | """, 37 | "SIRIUS_QUIZ_CORRECT": """ 38 | *Correct!!!* You win a ton or credits. 39 | """, 40 | "SIRIUS_QUIZ_INCORRECT": """ 41 | Sorry, this was the wrong answer. Don't take it too sirius. 42 | Better luck next time. 43 | """, 44 | "ORION_DESCRIPTION": """ 45 | You are on Orion. An icy world inhabited by monosyllabic furry sentients. 46 | """, 47 | "ORION_HIRE_COPILOT_QUESTION": """ 48 | You approach the natives at the spaceport 49 | One of them points at your hyperdrive and says 'Nice!' 50 | 51 | Do you want to hire them as a copilot? [yes/no] 52 | """, 53 | "ORION_NOTHING_GOING_ON": """ 54 | They are not very talkative. 55 | """, 56 | "BLACK_HOLE_DESCRIPTION": """ 57 | You are close to Black Hole #0997. Maybe coming here was a really stupid idea. 58 | """, 59 | "BLACK_HOLE_EXAMINE_QUESTION": """ 60 | Do you want to examine the black hole closer? [yes/no] 61 | """, 62 | "BLACK_HOLE_CRUNCHED": """ 63 | The black hole crunches you into a tiny piece of dust. 64 | 65 | THE END 66 | """, 67 | "BLACK_HOLE_COPILOT_SAVES_YOU": """ 68 | On the rim of the black hole your copilot blurts out: 69 | 70 | Turn left! 71 | 72 | You ignite the next-gen hyperdrive, creating a time-space anomaly. 73 | You are transported to an orthogonal dimension. 74 | """, 75 | "ORACLE_QUESTION": """ 76 | You have reached the cosmic oracle. With a booming telepathic voice it proclaims: 77 | 78 | What is the answer to life, the universe and everything? 79 | 80 | What do you reply? 81 | """, 82 | "ORACLE_CORRECT": """ 83 | You are worthy of our knowledge. In this crystal you will find a matter conversion tutorial. 84 | Return to your people in peace. 85 | 86 | A portal opens in front of you. 87 | """, 88 | "ORACLE_INCORRECT": """ 89 | You have much to learn still. Return to your people in peace. But we will keep that funny toy of yours. 90 | 91 | A portal opens in front of you. 92 | """, 93 | "END_CREDITS": """ 94 | You return from your adventure wise and famous. 95 | You have found the saged and fabulous crystal. Once decyphered, 96 | it will advance the knowledge of all sentient beings by generations. 97 | 98 | It may also look good in your cockpit. 99 | 100 | THE END 101 | """, 102 | } 103 | -------------------------------------------------------------------------------- /advanced/08-extract-data-structures/space_game.py: -------------------------------------------------------------------------------- 1 | """ 2 | Space Travel Game 3 | 4 | A simple text adventure written for a refactoring tutorial. 5 | """ 6 | from text_en import TEXT 7 | 8 | 9 | STARMAP = { 10 | 'earth': ['centauri', 'sirius'], 11 | 'centauri': ['earth', 'orion'], 12 | 'sirius': ["orion", "earth", "BH#0997"], 13 | 'orion': ["centauri", "sirius"], 14 | 'BH#0997': ['sirius'], 15 | 'oracle': ['earth'] 16 | } 17 | 18 | 19 | def display_inventory(flags): 20 | print(TEXT["BAR"]) 21 | if 'credits' in flags: 22 | print("You have plenty of stellar credits.") 23 | if 'engines' in flags: 24 | print("You have a brand new next-gen hyperdrive.") 25 | if 'copilot' in flags: 26 | print("A furry tech-savvy copilot is on board.") 27 | 28 | 29 | def display_destinations(destinations): 30 | print("\nWhere do you want to travel?") 31 | position = 1 32 | for d in destinations: 33 | print(f"[{position}] {d}") 34 | position += 1 35 | 36 | 37 | def select_planet(destinations): 38 | # choose the next planet 39 | travel_to = None 40 | while travel_to not in destinations: 41 | text = input() 42 | try: 43 | index = int(text) 44 | travel_to = destinations[index - 1] 45 | except ValueError: 46 | print("please enter a number") 47 | except IndexError: 48 | print(f"please enter 1-{len(destinations)}") 49 | return travel_to 50 | 51 | 52 | def visit_planet(planet, flags): 53 | 54 | if planet == "earth": 55 | print(TEXT["EARTH_DESCRIPTION"]) 56 | 57 | if planet == "centauri": 58 | print(TEXT["CENTAURI_DESCRIPTION"]) 59 | 60 | if not 'engines' in flags: 61 | print(TEXT["HYPERDRIVE_SHOPPING_QUESTION"]) 62 | if input() == "yes": 63 | if 'credits' in flags: 64 | print(TEXT["HYPERDRIVE_SHOPPING_SUCCESS"]) 65 | flags.add('engines') 66 | else: 67 | print(TEXT["HYPERDRIVE_TOO_EXPENSIVE"]) 68 | 69 | if planet == "sirius": 70 | print(TEXT["SIRIUS_DESCRIPTION"]) 71 | 72 | if not 'credits' in flags: 73 | print(TEXT["SIRIUS_QUIZ_QUESTION"]) 74 | answer = input() 75 | if answer == "2": 76 | print(TEXT["SIRIUS_QUIZ_CORRECT"]) 77 | flags.add('credits') 78 | else: 79 | print(TEXT["SIRIUS_QUIZ_INCORRECT"]) 80 | 81 | if planet == "orion": 82 | 83 | if 'engines' in flags and not 'copilot' in flags: 84 | print(TEXT["ORION_DESCRIPTION"]) 85 | print(TEXT["ORION_HIRE_COPILOT_QUESTION"]) 86 | if input() == "yes": 87 | flags.add('copilot') 88 | else: 89 | print(TEXT["ORION_DESCRIPTION"]) 90 | print(TEXT["ORION_NOTHING_GOING_ON"]) 91 | 92 | if planet == "BH#0997": 93 | print(TEXT["BLACK_HOLE_DESCRIPTION"]) 94 | 95 | if input(TEXT["BLACK_HOLE_EXAMINE_QUESTION"]) == "yes": 96 | if 'engines' in flags and 'copilot' in flags: 97 | print(TEXT["BLACK_HOLE_COPILOT_SAVES_YOU"]) 98 | planet = "oracle" 99 | else: 100 | print(TEXT["BLACK_HOLE_CRUNCHED"]) 101 | flags.add('dead') 102 | 103 | if planet == "oracle": 104 | print(TEXT["ORACLE_QUESTION"]) 105 | answer = input() 106 | if answer == "42": 107 | print(TEXT["ORACLE_CORRECT"]) 108 | flags.add('crystal_found') 109 | else: 110 | print(TEXT["ORACLE_INCORRECT"]) 111 | flags.remove('engines') 112 | 113 | return planet 114 | 115 | 116 | def travel(): 117 | 118 | print(TEXT["OPENING_MESSAGE"]) 119 | 120 | planet = "earth" 121 | flags = set() 122 | 123 | while not ('crystal_found' in flags or 'dead' in flags): 124 | 125 | display_inventory(flags) 126 | planet = visit_planet(planet, flags) 127 | 128 | if not 'dead' in flags: 129 | destinations = STARMAP[planet] 130 | display_destinations(destinations) 131 | planet = select_planet(destinations) 132 | 133 | print(TEXT["END_CREDITS"]) 134 | 135 | 136 | if __name__ == "__main__": 137 | travel() 138 | -------------------------------------------------------------------------------- /advanced/08-extract-data-structures/test_space_game.py: -------------------------------------------------------------------------------- 1 | import io 2 | import pytest 3 | 4 | from space_game import travel 5 | 6 | 7 | # the actual solution to the game 8 | SOLUTION = [ 9 | "2", 10 | "2", # go to sirius and win quiz 11 | "1", 12 | "1", 13 | "yes", # go to centauri and buy GPU drive 14 | "2", 15 | "yes", # hire copilot on orion 16 | "2", 17 | "3", 18 | "yes", # jump into black hole 19 | "42", 20 | "1", # get crystal 21 | ] 22 | 23 | DIE_BY_BLACK_HOLE = [ 24 | "2", 25 | "2", # go to sirius and win quiz 26 | "1", 27 | "1", 28 | "yes", # go to centauri and buy GPU drive 29 | "2", 30 | "no", # hire copilot on orion 31 | "2", 32 | "3", 33 | "yes", # jump into black hole 34 | ] 35 | 36 | # text sniplets that should appear literally in the output 37 | PHRASES = [ 38 | "The stars are waiting for you", 39 | "Betelgeuse", 40 | "credits", 41 | "tech-savvy copilot", 42 | "buy", 43 | "Do you want to hire them", 44 | "Black Hole", 45 | "stupid idea", 46 | "Return to your people in peace", 47 | "THE END", 48 | ] 49 | 50 | 51 | @pytest.fixture 52 | def solution_input(): 53 | """helper function to hijack the keyboard for testing""" 54 | return io.StringIO("\n".join(SOLUTION)) 55 | 56 | 57 | def test_travel(monkeypatch, solution_input): 58 | """game finishes""" 59 | monkeypatch.setattr("sys.stdin", solution_input) 60 | travel() 61 | 62 | 63 | def test_output(monkeypatch, capsys, solution_input): 64 | """text output is not empty""" 65 | monkeypatch.setattr("sys.stdin", solution_input) 66 | 67 | travel() 68 | 69 | captured = capsys.readouterr() 70 | assert len(captured.out) > 0 71 | 72 | 73 | def test_die(monkeypatch, capsys): 74 | """player dies""" 75 | monkeypatch.setattr("sys.stdin", io.StringIO("\n".join(DIE_BY_BLACK_HOLE))) 76 | 77 | travel() 78 | 79 | captured = capsys.readouterr() 80 | assert "crunches" in captured.out 81 | 82 | 83 | @pytest.mark.parametrize("phrase", PHRASES) 84 | def test_output_phrases(monkeypatch, capsys, solution_input, phrase): 85 | """check for some key phrases in the output""" 86 | monkeypatch.setattr("sys.stdin", solution_input) 87 | 88 | travel() 89 | 90 | captured = capsys.readouterr() 91 | assert phrase in captured.out 92 | -------------------------------------------------------------------------------- /advanced/08-extract-data-structures/text_en.py: -------------------------------------------------------------------------------- 1 | TEXT = { 2 | "OPENING_MESSAGE": """ 3 | ********************************************************************* 4 | 5 | You and your trusted spaceship set out to look for 6 | fame, wisdom, and adventure. 7 | The stars are waiting for you. 8 | 9 | """, 10 | "BAR": "\n*********************************************************************\n", 11 | "EARTH_DESCRIPTION": "\nYou are on Earth. The cradle of humankind. A rather dull place.", 12 | "CENTAURI_DESCRIPTION": "\nYou are on Alpha Centauri. A buzzing trade hub.\nYou can buy almost anything here.", 13 | "HYPERDRIVE_SHOPPING_QUESTION": """ 14 | They have a brand new next-generation hyperdrive with a built-in GPU. 15 | 16 | Would you like to buy one [yes/no]""", 17 | "HYPERDRIVE_TOO_EXPENSIVE": """ 18 | You cannot afford it. The GPU is too expensive.""", 19 | "HYPERDRIVE_SHOPPING_SUCCESS": """ 20 | Your spaceship now has a shiny hyperdrive on both sides. 21 | """, 22 | "SIRIUS_DESCRIPTION": """ 23 | You are on Sirius. The system is full of media companies 24 | and content delivery networks. 25 | """, 26 | "SIRIUS_QUIZ_QUESTION": """ 27 | You manage to get a place in *Stellar* - the greatest quiz show in the universe. 28 | And here is your question: 29 | 30 | Which star do you find on the shoulder of Orion? 31 | 32 | [1] Altair 33 | [2] Betelgeuse 34 | [3] Aldebaran 35 | [4] Andromeda 36 | """, 37 | "SIRIUS_QUIZ_CORRECT": """ 38 | *Correct!!!* You win a ton or credits. 39 | """, 40 | "SIRIUS_QUIZ_INCORRECT": """ 41 | Sorry, this was the wrong answer. Don't take it too sirius. 42 | Better luck next time. 43 | """, 44 | "ORION_DESCRIPTION": """ 45 | You are on Orion. An icy world inhabited by monosyllabic furry sentients. 46 | """, 47 | "ORION_HIRE_COPILOT_QUESTION": """ 48 | You approach the natives at the spaceport 49 | One of them points at your hyperdrive and says 'Nice!' 50 | 51 | Do you want to hire them as a copilot? [yes/no] 52 | """, 53 | "ORION_NOTHING_GOING_ON": """ 54 | They are not very talkative. 55 | """, 56 | "BLACK_HOLE_DESCRIPTION": """ 57 | You are close to Black Hole #0997. Maybe coming here was a really stupid idea. 58 | """, 59 | "BLACK_HOLE_EXAMINE_QUESTION": """ 60 | Do you want to examine the black hole closer? [yes/no] 61 | """, 62 | "BLACK_HOLE_CRUNCHED": """ 63 | The black hole crunches you into a tiny piece of dust. 64 | 65 | THE END 66 | """, 67 | "BLACK_HOLE_COPILOT_SAVES_YOU": """ 68 | On the rim of the black hole your copilot blurts out: 69 | 70 | Turn left! 71 | 72 | You ignite the next-gen hyperdrive, creating a time-space anomaly. 73 | You are transported to an orthogonal dimension. 74 | """, 75 | "ORACLE_QUESTION": """ 76 | You have reached the cosmic oracle. With a booming telepathic voice it proclaims: 77 | 78 | What is the answer to life, the universe and everything? 79 | 80 | What do you reply? 81 | """, 82 | "ORACLE_CORRECT": """ 83 | You are worthy of our knowledge. In this crystal you will find a matter conversion tutorial. 84 | Return to your people in peace. 85 | 86 | A portal opens in front of you. 87 | """, 88 | "ORACLE_INCORRECT": """ 89 | You have much to learn still. Return to your people in peace. But we will keep that funny toy of yours. 90 | 91 | A portal opens in front of you. 92 | """, 93 | "END_CREDITS": """ 94 | You return from your adventure wise and famous. 95 | You have found the saged and fabulous crystal. Once decyphered, 96 | it will advance the knowledge of all sentient beings by generations. 97 | 98 | It may also look good in your cockpit. 99 | 100 | THE END 101 | """, 102 | } 103 | -------------------------------------------------------------------------------- /advanced/09-extract-class/space_game.py: -------------------------------------------------------------------------------- 1 | """ 2 | Space Travel Game 3 | 4 | A simple text adventure written for a refactoring tutorial. 5 | """ 6 | from text_en import TEXT 7 | 8 | 9 | STARMAP = { 10 | 'earth': ['centauri', 'sirius'], 11 | 'centauri': ['earth', 'orion'], 12 | 'sirius': ["orion", "earth", "black_hole"], 13 | 'orion': ["centauri", "sirius"], 14 | 'black_hole': ['sirius'], 15 | 'oracle': ['earth'] 16 | } 17 | 18 | # 19 | # Puzzle functions 20 | # 21 | def buy_engine(flags): 22 | if not 'engines' in flags: 23 | print(TEXT["HYPERDRIVE_SHOPPING_QUESTION"]) 24 | if input() == "yes": 25 | if 'credits' in flags: 26 | print(TEXT["HYPERDRIVE_SHOPPING_SUCCESS"]) 27 | flags.add('engines') 28 | else: 29 | print(TEXT["HYPERDRIVE_TOO_EXPENSIVE"]) 30 | 31 | 32 | def quiz_show(flags): 33 | if not 'credits' in flags: 34 | print(TEXT["SIRIUS_QUIZ_QUESTION"]) 35 | answer = input() 36 | if answer == "2": 37 | print(TEXT["SIRIUS_QUIZ_CORRECT"]) 38 | flags.add('credits') 39 | else: 40 | print(TEXT["SIRIUS_QUIZ_INCORRECT"]) 41 | 42 | 43 | def hire_copilot(flags): 44 | if 'engines' in flags and not 'copilot' in flags: 45 | print(TEXT["ORION_HIRE_COPILOT_QUESTION"]) 46 | if input() == "yes": 47 | flags.add('copilot') 48 | else: 49 | print(TEXT["ORION_NOTHING_GOING_ON"]) 50 | 51 | 52 | def examine_black_hole(flags): 53 | if input(TEXT["BLACK_HOLE_EXAMINE_QUESTION"]) == "yes": 54 | if 'engines' in flags and 'copilot' in flags: 55 | print(TEXT["BLACK_HOLE_COPILOT_SAVES_YOU"]) 56 | 57 | print(TEXT["ORACLE_QUESTION"]) 58 | answer = input() 59 | if answer == "42": 60 | print(TEXT["ORACLE_CORRECT"]) 61 | flags.add('crystal_found') 62 | self.destinations = ['earth'] 63 | else: 64 | print(TEXT["ORACLE_INCORRECT"]) 65 | flags.remove('engines') 66 | 67 | else: 68 | print(TEXT["BLACK_HOLE_CRUNCHED"]) 69 | flags.add('dead') 70 | 71 | 72 | class Planet: 73 | 74 | def __init__(self, name, puzzle=None): 75 | self.name = name 76 | self.description = TEXT[name.upper() + '_DESCRIPTION'] 77 | self.destinations = STARMAP[self.name] 78 | self.puzzle = puzzle 79 | 80 | def visit(self, flags): 81 | print(self.description) 82 | if self.puzzle: 83 | puzzle() 84 | 85 | def display_destinations(self): 86 | print("\nWhere do you want to travel?") 87 | position = 1 88 | for d in self.destinations: 89 | print(f"[{position}] {d}") 90 | position += 1 91 | 92 | 93 | PLANETS = {name: Planet(name) for name in ['earth', 'sirius', 'centauri', 'orion', 'black_hole']} 94 | 95 | 96 | def display_inventory(flags): 97 | print(TEXT["BAR"]) 98 | if 'credits' in flags: 99 | print("You have plenty of stellar credits.") 100 | if 'engines' in flags: 101 | print("You have a brand new next-gen hyperdrive.") 102 | if 'copilot' in flags: 103 | print("A furry tech-savvy copilot is on board.") 104 | 105 | 106 | 107 | def select_planet(destinations): 108 | # choose the next planet 109 | travel_to = None 110 | while travel_to not in destinations: 111 | text = input() 112 | try: 113 | index = int(text) 114 | travel_to = destinations[index - 1] 115 | except ValueError: 116 | print("please enter a number") 117 | except IndexError: 118 | print(f"please enter 1-{len(destinations)}") 119 | return PLANETS[travel_to] 120 | 121 | 122 | 123 | def travel(): 124 | 125 | print(TEXT["OPENING_MESSAGE"]) 126 | 127 | planet = PLANETS["earth"] 128 | flags = set() 129 | 130 | while not ('crystal_found' in flags or 'dead' in flags): 131 | 132 | display_inventory(flags) 133 | planet.visit(flags) 134 | 135 | if not 'dead' in flags: 136 | planet.display_destinations() 137 | planet = select_planet(planet.destinations) 138 | 139 | if 'crystal_found' in flags: 140 | print(TEXT["END_CREDITS"]) 141 | 142 | 143 | if __name__ == "__main__": 144 | travel() 145 | -------------------------------------------------------------------------------- /advanced/09-extract-class/test_space_game.py: -------------------------------------------------------------------------------- 1 | import io 2 | import pytest 3 | 4 | from space_game import travel 5 | 6 | 7 | # the actual solution to the game 8 | SOLUTION = [ 9 | "2", 10 | "2", # go to sirius and win quiz 11 | "1", 12 | "1", 13 | "yes", # go to centauri and buy GPU drive 14 | "2", 15 | "yes", # hire copilot on orion 16 | "2", 17 | "3", 18 | "yes", # jump into black hole 19 | "42", 20 | "1", # get crystal 21 | ] 22 | 23 | DIE_BY_BLACK_HOLE = [ 24 | "2", 25 | "2", # go to sirius and win quiz 26 | "1", 27 | "1", 28 | "yes", # go to centauri and buy GPU drive 29 | "2", 30 | "no", # hire copilot on orion 31 | "2", 32 | "3", 33 | "yes", # jump into black hole 34 | ] 35 | 36 | # text sniplets that should appear literally in the output 37 | PHRASES = [ 38 | "The stars are waiting for you", 39 | "Betelgeuse", 40 | "credits", 41 | "tech-savvy copilot", 42 | "buy", 43 | "Do you want to hire them", 44 | "Black Hole", 45 | "stupid idea", 46 | "Return to your people in peace", 47 | "THE END", 48 | ] 49 | 50 | 51 | @pytest.fixture 52 | def solution_input(): 53 | """helper function to hijack the keyboard for testing""" 54 | return io.StringIO("\n".join(SOLUTION)) 55 | 56 | 57 | def test_travel(monkeypatch, solution_input): 58 | """game finishes""" 59 | monkeypatch.setattr("sys.stdin", solution_input) 60 | travel() 61 | 62 | 63 | def test_output(monkeypatch, capsys, solution_input): 64 | """text output is not empty""" 65 | monkeypatch.setattr("sys.stdin", solution_input) 66 | 67 | travel() 68 | 69 | captured = capsys.readouterr() 70 | assert len(captured.out) > 0 71 | 72 | 73 | def test_die(monkeypatch, capsys): 74 | """player dies""" 75 | monkeypatch.setattr("sys.stdin", io.StringIO("\n".join(DIE_BY_BLACK_HOLE))) 76 | 77 | travel() 78 | 79 | captured = capsys.readouterr() 80 | assert "crunches" in captured.out 81 | 82 | 83 | @pytest.mark.parametrize("phrase", PHRASES) 84 | def test_output_phrases(monkeypatch, capsys, solution_input, phrase): 85 | """check for some key phrases in the output""" 86 | monkeypatch.setattr("sys.stdin", solution_input) 87 | 88 | travel() 89 | 90 | captured = capsys.readouterr() 91 | assert phrase in captured.out 92 | -------------------------------------------------------------------------------- /advanced/09-extract-class/text_en.py: -------------------------------------------------------------------------------- 1 | TEXT = { 2 | "OPENING_MESSAGE": """ 3 | ********************************************************************* 4 | 5 | You and your trusted spaceship set out to look for 6 | fame, wisdom, and adventure. 7 | The stars are waiting for you. 8 | 9 | """, 10 | "BAR": "\n*********************************************************************\n", 11 | "EARTH_DESCRIPTION": "\nYou are on Earth. The cradle of humankind. A rather dull place.", 12 | "CENTAURI_DESCRIPTION": "\nYou are on Alpha Centauri. A buzzing trade hub.\nYou can buy almost anything here.", 13 | "HYPERDRIVE_SHOPPING_QUESTION": """ 14 | They have a brand new next-generation hyperdrive with a built-in GPU. 15 | 16 | Would you like to buy one [yes/no]""", 17 | "HYPERDRIVE_TOO_EXPENSIVE": """ 18 | You cannot afford it. The GPU is too expensive.""", 19 | "HYPERDRIVE_SHOPPING_SUCCESS": """ 20 | Your spaceship now has a shiny hyperdrive on both sides. 21 | """, 22 | "SIRIUS_DESCRIPTION": """ 23 | You are on Sirius. The system is full of media companies 24 | and content delivery networks. 25 | """, 26 | "SIRIUS_QUIZ_QUESTION": """ 27 | You manage to get a place in *Stellar* - the greatest quiz show in the universe. 28 | And here is your question: 29 | 30 | Which star do you find on the shoulder of Orion? 31 | 32 | [1] Altair 33 | [2] Betelgeuse 34 | [3] Aldebaran 35 | [4] Andromeda 36 | """, 37 | "SIRIUS_QUIZ_CORRECT": """ 38 | *Correct!!!* You win a ton or credits. 39 | """, 40 | "SIRIUS_QUIZ_INCORRECT": """ 41 | Sorry, this was the wrong answer. Don't take it too sirius. 42 | Better luck next time. 43 | """, 44 | "ORION_DESCRIPTION": """ 45 | You are on Orion. An icy world inhabited by monosyllabic furry sentients. 46 | """, 47 | "ORION_HIRE_COPILOT_QUESTION": """ 48 | You approach the natives at the spaceport 49 | One of them points at your hyperdrive and says 'Nice!' 50 | 51 | Do you want to hire them as a copilot? [yes/no] 52 | """, 53 | "ORION_NOTHING_GOING_ON": """ 54 | They are not very talkative. 55 | """, 56 | "BLACK_HOLE_DESCRIPTION": """ 57 | You are close to Black Hole #0997. Maybe coming here was a really stupid idea. 58 | """, 59 | "BLACK_HOLE_EXAMINE_QUESTION": """ 60 | Do you want to examine the black hole closer? [yes/no] 61 | """, 62 | "BLACK_HOLE_CRUNCHED": """ 63 | The black hole crunches you into a tiny piece of dust. 64 | 65 | THE END 66 | """, 67 | "BLACK_HOLE_COPILOT_SAVES_YOU": """ 68 | On the rim of the black hole your copilot blurts out: 69 | 70 | Turn left! 71 | 72 | You ignite the next-gen hyperdrive, creating a time-space anomaly. 73 | You are transported to an orthogonal dimension. 74 | """, 75 | "ORACLE_QUESTION": """ 76 | You have reached the cosmic oracle. With a booming telepathic voice it proclaims: 77 | 78 | What is the answer to life, the universe and everything? 79 | 80 | What do you reply? 81 | """, 82 | "ORACLE_CORRECT": """ 83 | You are worthy of our knowledge. In this crystal you will find a matter conversion tutorial. 84 | Return to your people in peace. 85 | 86 | A portal opens in front of you. 87 | """, 88 | "ORACLE_INCORRECT": """ 89 | You have much to learn still. Return to your people in peace. But we will keep that funny toy of yours. 90 | 91 | A portal opens in front of you. 92 | """, 93 | "END_CREDITS": """ 94 | You return from your adventure wise and famous. 95 | You have found the saged and fabulous crystal. Once decyphered, 96 | it will advance the knowledge of all sentient beings by generations. 97 | 98 | It may also look good in your cockpit. 99 | 100 | THE END 101 | """, 102 | } 103 | -------------------------------------------------------------------------------- /advanced/10-final-cleanup/space_game.py: -------------------------------------------------------------------------------- 1 | """ 2 | Space Travel Game 3 | 4 | A simple text adventure written for a refactoring tutorial. 5 | """ 6 | from text_en import TEXT 7 | 8 | 9 | # 10 | # Puzzle functions 11 | # 12 | def buy_engine(flags): 13 | if not 'engines' in flags: 14 | print(TEXT["HYPERDRIVE_SHOPPING_QUESTION"]) 15 | if input() == "yes": 16 | if 'credits' in flags: 17 | print(TEXT["HYPERDRIVE_SHOPPING_SUCCESS"]) 18 | flags.add('engines') 19 | else: 20 | print(TEXT["HYPERDRIVE_TOO_EXPENSIVE"]) 21 | 22 | 23 | def quiz_show(flags): 24 | if not 'credits' in flags: 25 | print(TEXT["SIRIUS_QUIZ_QUESTION"]) 26 | answer = input() 27 | if answer == "2": 28 | print(TEXT["SIRIUS_QUIZ_CORRECT"]) 29 | flags.add('credits') 30 | else: 31 | print(TEXT["SIRIUS_QUIZ_INCORRECT"]) 32 | 33 | 34 | def hire_copilot(flags): 35 | if 'engines' in flags and not 'copilot' in flags: 36 | print(TEXT["ORION_HIRE_COPILOT_QUESTION"]) 37 | if input() == "yes": 38 | flags.add('copilot') 39 | else: 40 | print(TEXT["ORION_NOTHING_GOING_ON"]) 41 | 42 | 43 | def examine_black_hole(flags): 44 | if input(TEXT["BLACK_HOLE_EXAMINE_QUESTION"]) == "yes": 45 | if 'engines' in flags and 'copilot' in flags: 46 | print(TEXT["BLACK_HOLE_COPILOT_SAVES_YOU"]) 47 | 48 | print(TEXT["ORACLE_QUESTION"]) 49 | answer = input() 50 | if answer == "42": 51 | print(TEXT["ORACLE_CORRECT"]) 52 | flags.add('game_end') 53 | print(TEXT["END_CREDITS"]) # skips last move 54 | else: 55 | print(TEXT["ORACLE_INCORRECT"]) 56 | flags.remove('engines') 57 | else: 58 | print(TEXT["BLACK_HOLE_CRUNCHED"]) 59 | flags.add('game_end') 60 | 61 | 62 | class Planet: 63 | 64 | def __init__(self, name, destinations, puzzle=None): 65 | self.name = name 66 | self.description = TEXT[name.upper() + '_DESCRIPTION'] 67 | self.destinations = destinations 68 | self.puzzle = puzzle 69 | 70 | def visit(self, flags): 71 | print(self.description) 72 | if self.puzzle: 73 | self.puzzle(flags) 74 | 75 | def display_destinations(self): 76 | print("\nWhere do you want to travel?") 77 | position = 1 78 | for d in self.destinations: 79 | print(f"[{position}] {d}") 80 | position += 1 81 | 82 | 83 | PLANETS = { 84 | 'earth': Planet('earth', ['centauri', 'sirius']), 85 | 'sirius': Planet('sirius', ["orion", "earth", "black_hole"],quiz_show), 86 | 'orion': Planet('orion', ["centauri", "sirius"], hire_copilot), 87 | 'centauri': Planet('centauri', ['earth', 'orion'], buy_engine), 88 | 'black_hole': Planet('black_hole', ['sirius'], examine_black_hole) 89 | } 90 | 91 | 92 | def display_inventory(flags): 93 | print(TEXT["BAR"]) 94 | if 'credits' in flags: 95 | print("You have plenty of stellar credits.") 96 | if 'engines' in flags: 97 | print("You have a brand new next-gen hyperdrive.") 98 | if 'copilot' in flags: 99 | print("A furry tech-savvy copilot is on board.") 100 | 101 | 102 | 103 | def select_planet(destinations): 104 | # choose the next planet 105 | travel_to = None 106 | while travel_to not in destinations: 107 | text = input() 108 | try: 109 | index = int(text) 110 | travel_to = destinations[index - 1] 111 | except ValueError: 112 | print("please enter a number") 113 | except IndexError: 114 | print(f"please enter 1-{len(destinations)}") 115 | return PLANETS[travel_to] 116 | 117 | 118 | 119 | def travel(): 120 | 121 | print(TEXT["OPENING_MESSAGE"]) 122 | flags = set() 123 | 124 | planet = PLANETS["earth"] 125 | planet.visit(flags) 126 | 127 | while not 'game_end' in flags: 128 | planet.display_destinations() 129 | planet = select_planet(planet.destinations) 130 | display_inventory(flags) 131 | planet.visit(flags) 132 | 133 | 134 | if __name__ == "__main__": 135 | travel() 136 | -------------------------------------------------------------------------------- /advanced/10-final-cleanup/test_space_game.py: -------------------------------------------------------------------------------- 1 | import io 2 | import pytest 3 | 4 | from space_game import travel 5 | 6 | 7 | # the actual solution to the game 8 | SOLUTION = [ 9 | "2", 10 | "2", # go to sirius and win quiz 11 | "1", 12 | "1", 13 | "yes", # go to centauri and buy GPU drive 14 | "2", 15 | "yes", # hire copilot on orion 16 | "2", 17 | "3", 18 | "yes", # jump into black hole 19 | "42", 20 | "1", # get crystal 21 | ] 22 | 23 | DIE_BY_BLACK_HOLE = [ 24 | "2", 25 | "2", # go to sirius and win quiz 26 | "1", 27 | "1", 28 | "yes", # go to centauri and buy GPU drive 29 | "2", 30 | "no", # hire copilot on orion 31 | "2", 32 | "3", 33 | "yes", # jump into black hole 34 | ] 35 | 36 | # text sniplets that should appear literally in the output 37 | PHRASES = [ 38 | "The stars are waiting for you", 39 | "Betelgeuse", 40 | "credits", 41 | "tech-savvy copilot", 42 | "buy", 43 | "Do you want to hire them", 44 | "Black Hole", 45 | "stupid idea", 46 | "Return to your people in peace", 47 | "THE END", 48 | ] 49 | 50 | 51 | @pytest.fixture 52 | def solution_input(): 53 | """helper function to hijack the keyboard for testing""" 54 | return io.StringIO("\n".join(SOLUTION)) 55 | 56 | 57 | def test_travel(monkeypatch, solution_input): 58 | """game finishes""" 59 | monkeypatch.setattr("sys.stdin", solution_input) 60 | travel() 61 | 62 | 63 | def test_output(monkeypatch, capsys, solution_input): 64 | """text output is not empty""" 65 | monkeypatch.setattr("sys.stdin", solution_input) 66 | 67 | travel() 68 | 69 | captured = capsys.readouterr() 70 | assert len(captured.out) > 0 71 | 72 | 73 | def test_die(monkeypatch, capsys): 74 | """player dies""" 75 | monkeypatch.setattr("sys.stdin", io.StringIO("\n".join(DIE_BY_BLACK_HOLE))) 76 | 77 | travel() 78 | 79 | captured = capsys.readouterr() 80 | assert "crunches" in captured.out 81 | 82 | 83 | @pytest.mark.parametrize("phrase", PHRASES) 84 | def test_output_phrases(monkeypatch, capsys, solution_input, phrase): 85 | """check for some key phrases in the output""" 86 | monkeypatch.setattr("sys.stdin", solution_input) 87 | 88 | travel() 89 | 90 | captured = capsys.readouterr() 91 | assert phrase in captured.out 92 | -------------------------------------------------------------------------------- /advanced/10-final-cleanup/text_en.py: -------------------------------------------------------------------------------- 1 | TEXT = { 2 | "OPENING_MESSAGE": """ 3 | ********************************************************************* 4 | 5 | You and your trusted spaceship set out to look for 6 | fame, wisdom, and adventure. 7 | The stars are waiting for you. 8 | 9 | """, 10 | "BAR": "\n*********************************************************************\n", 11 | "EARTH_DESCRIPTION": "\nYou are on Earth. The cradle of humankind. A rather dull place.", 12 | "CENTAURI_DESCRIPTION": "\nYou are on Alpha Centauri. A buzzing trade hub.\nYou can buy almost anything here.", 13 | "HYPERDRIVE_SHOPPING_QUESTION": """ 14 | They have a brand new next-generation hyperdrive with a built-in GPU. 15 | 16 | Would you like to buy one [yes/no]""", 17 | "HYPERDRIVE_TOO_EXPENSIVE": """ 18 | You cannot afford it. The GPU is too expensive.""", 19 | "HYPERDRIVE_SHOPPING_SUCCESS": """ 20 | Your spaceship now has a shiny hyperdrive on both sides. 21 | """, 22 | "SIRIUS_DESCRIPTION": """ 23 | You are on Sirius. The system is full of media companies 24 | and content delivery networks. 25 | """, 26 | "SIRIUS_QUIZ_QUESTION": """ 27 | You manage to get a place in *Stellar* - the greatest quiz show in the universe. 28 | And here is your question: 29 | 30 | Which star do you find on the shoulder of Orion? 31 | 32 | [1] Altair 33 | [2] Betelgeuse 34 | [3] Aldebaran 35 | [4] Andromeda 36 | """, 37 | "SIRIUS_QUIZ_CORRECT": """ 38 | *Correct!!!* You win a ton or credits. 39 | """, 40 | "SIRIUS_QUIZ_INCORRECT": """ 41 | Sorry, this was the wrong answer. Don't take it too sirius. 42 | Better luck next time. 43 | """, 44 | "ORION_DESCRIPTION": """ 45 | You are on Orion. An icy world inhabited by monosyllabic furry sentients. 46 | """, 47 | "ORION_HIRE_COPILOT_QUESTION": """ 48 | You approach the natives at the spaceport 49 | One of them points at your hyperdrive and says 'Nice!' 50 | 51 | Do you want to hire them as a copilot? [yes/no] 52 | """, 53 | "ORION_NOTHING_GOING_ON": """ 54 | They are not very talkative. 55 | """, 56 | "BLACK_HOLE_DESCRIPTION": """ 57 | You are close to Black Hole #0997. Maybe coming here was a really stupid idea. 58 | """, 59 | "BLACK_HOLE_EXAMINE_QUESTION": """ 60 | Do you want to examine the black hole closer? [yes/no] 61 | """, 62 | "BLACK_HOLE_CRUNCHED": """ 63 | The black hole crunches you into a tiny piece of dust. 64 | 65 | THE END 66 | """, 67 | "BLACK_HOLE_COPILOT_SAVES_YOU": """ 68 | On the rim of the black hole your copilot blurts out: 69 | 70 | Turn left! 71 | 72 | You ignite the next-gen hyperdrive, creating a time-space anomaly. 73 | You are transported to an orthogonal dimension. 74 | """, 75 | "ORACLE_QUESTION": """ 76 | You have reached the cosmic oracle. With a booming telepathic voice it proclaims: 77 | 78 | What is the answer to life, the universe and everything? 79 | 80 | What do you reply? 81 | """, 82 | "ORACLE_CORRECT": """ 83 | You are worthy of our knowledge. In this crystal you will find a matter conversion tutorial. 84 | Return to your people in peace. 85 | 86 | A portal opens in front of you. 87 | """, 88 | "ORACLE_INCORRECT": """ 89 | You have much to learn still. Return to your people in peace. But we will keep that funny toy of yours. 90 | 91 | A portal opens in front of you. 92 | """, 93 | "END_CREDITS": """ 94 | You return from your adventure wise and famous. 95 | You have found the saged and fabulous crystal. Once decyphered, 96 | it will advance the knowledge of all sentient beings by generations. 97 | 98 | It may also look good in your cockpit. 99 | 100 | THE END 101 | """, 102 | } 103 | -------------------------------------------------------------------------------- /advanced/space_game.py: -------------------------------------------------------------------------------- 1 | """ 2 | Space Travel Game 3 | 4 | A simple text adventure written for a refactoring tutorial. 5 | 6 | DIRECTORS CUT - SLIGHTLY MORE COMPLEX EXERCISE 7 | """ 8 | 9 | TEXT = { 10 | "OPENING_MESSAGE": """ 11 | ********************************************************************* 12 | 13 | You and your trusted spaceship set out to look for 14 | fame, wisdom, and adventure. 15 | The stars are waiting for you. 16 | 17 | """, 18 | "BAR": "\n*********************************************************************\n", 19 | "EARTH_DESCRIPTION": "\nYou are on Earth. The cradle of humankind. A rather dull place.", 20 | "CENTAURI_DESCRIPTION": "\nYou are on Alpha Centauri. A buzzing trade hub.\nYou can buy almost anything here.", 21 | "HYPERDRIVE_SHOPPING_QUESTION": """ 22 | They have a brand new next-generation hyperdrive with a built-in GPU. 23 | 24 | Would you like to buy one [yes/no]""", 25 | "HYPERDRIVE_TOO_EXPENSIVE": """ 26 | You cannot afford it. The GPU is too expensive.""", 27 | "HYPERDRIVE_SHOPPING_SUCCESS": """ 28 | Your spaceship now has a shiny hyperdrive on both sides. 29 | """, 30 | "SIRIUS_DESCRIPTION": """ 31 | You are on Sirius. The system is full of media companies 32 | and content delivery networks. 33 | """, 34 | "SIRIUS_QUIZ_QUESTION": """ 35 | You manage to get a place in *Stellar* - the greatest quiz show in the universe. 36 | And here is your question: 37 | 38 | Which star do you find on the shoulder of Orion? 39 | 40 | [1] Altair 41 | [2] Betelgeuse 42 | [3] Aldebaran 43 | [4] Andromeda 44 | """, 45 | "SIRIUS_QUIZ_CORRECT": """ 46 | *Correct!!!* You win a ton or credits. 47 | """, 48 | "SIRIUS_QUIZ_INCORRECT": """ 49 | Sorry, this was the wrong answer. Don't take it too sirius. 50 | Better luck next time. 51 | """, 52 | "ORION_DESCRIPTION": """ 53 | You are on Orion. An icy world inhabited by monosyllabic furry sentients. 54 | """, 55 | "ORION_HIRE_COPILOT_QUESTION": """ 56 | You approach the natives at the spaceport 57 | One of them points at your hyperdrive and says 'Nice!' 58 | 59 | Do you want to hire them as a copilot? [yes/no] 60 | """, 61 | "ORION_NOTHING_GOING_ON": """ 62 | They are not very talkative. 63 | """, 64 | "BLACK_HOLE_DESCRIPTION": """ 65 | You are close to Black Hole #0997. Maybe coming here was a really stupid idea. 66 | """, 67 | "BLACK_HOLE_EXAMINE_QUESTION": """ 68 | Do you want to examine the black hole closer? [yes/no] 69 | """, 70 | "BLACK_HOLE_CRUNCHED": """ 71 | The black hole crunches you into a tiny piece of dust. 72 | 73 | THE END 74 | """, 75 | "BLACK_HOLE_COPILOT_SAVES_YOU": """ 76 | On the rim of the black hole your copilot blurts out: 77 | 78 | Turn left! 79 | 80 | You ignite the next-gen hyperdrive, creating a time-space anomaly. 81 | You are transported to an orthogonal dimension. 82 | """, 83 | "ORACLE_QUESTION": """ 84 | You have reached the cosmic oracle. With a booming telepathic voice it proclaims: 85 | 86 | What is the answer to life, the universe and everything? 87 | 88 | What do you reply? 89 | """, 90 | "ORACLE_CORRECT": """ 91 | You are worthy of our knowledge. In this crystal you will find a matter conversion tutorial. 92 | Return to your people in peace. 93 | 94 | A portal opens in front of you. 95 | """, 96 | "ORACLE_INCORRECT": """ 97 | You have much to learn still. Return to your people in peace. But we will keep that funny toy of yours. 98 | 99 | A portal opens in front of you. 100 | """, 101 | "END_CREDITS": """ 102 | You return from your adventure wise and famous. 103 | You have found the saged and fabulous crystal. Once decyphered, 104 | it will advance the knowledge of all sentient beings by generations. 105 | 106 | It may also look good in your cockpit. 107 | 108 | THE END 109 | """, 110 | } 111 | 112 | 113 | def travel(): 114 | 115 | print(TEXT["OPENING_MESSAGE"]) 116 | 117 | planet = "earth" 118 | engines = False 119 | copilot = False 120 | credits = False 121 | crystal_found = False 122 | 123 | while not crystal_found: 124 | 125 | print(TEXT["BAR"]) 126 | 127 | # display inventory 128 | if credits: 129 | print("You have plenty of stellar credits.") 130 | if engines: 131 | print("You have a brand new next-gen hyperdrive.") 132 | if copilot: 133 | print("A furry tech-savvy copilot is on board.") 134 | 135 | # 136 | # interaction with planets 137 | # 138 | if planet == "earth": 139 | destinations = ["centauri", "sirius"] 140 | print(TEXT["EARTH_DESCRIPTION"]) 141 | 142 | if planet == "centauri": 143 | print(TEXT["CENTAURI_DESCRIPTION"]) 144 | destinations = ["earth", "orion"] 145 | 146 | if not engines: 147 | print(TEXT["HYPERDRIVE_SHOPPING_QUESTION"]) 148 | if input() == "yes": 149 | if credits: 150 | print(TEXT["HYPERDRIVE_SHOPPING_SUCCESS"]) 151 | engines = True 152 | else: 153 | print(TEXT["HYPERDRIVE_TOO_EXPENSIVE"]) 154 | 155 | if planet == "sirius": 156 | print(TEXT["SIRIUS_DESCRIPTION"]) 157 | destinations = ["orion", "earth", "BH#0997"] 158 | 159 | if not credits: 160 | print(TEXT["SIRIUS_QUIZ_QUESTION"]) 161 | answer = input() 162 | if answer == "2": 163 | print(TEXT["SIRIUS_QUIZ_CORRECT"]) 164 | credits = True 165 | else: 166 | print(TEXT["SIRIUS_QUIZ_INCORRECT"]) 167 | 168 | if planet == "orion": 169 | destinations = ["centauri", "sirius"] 170 | 171 | if engines and not copilot: 172 | print(TEXT["ORION_DESCRIPTION"]) 173 | print(TEXT["ORION_HIRE_COPILOT_QUESTION"]) 174 | if input() == "yes": 175 | copilot = True 176 | else: 177 | print(TEXT["ORION_DESCRIPTION"]) 178 | print(TEXT["ORION_NOTHING_GOING_ON"]) 179 | 180 | if planet == "BH#0997": 181 | print(TEXT["BLACK_HOLE_DESCRIPTION"]) 182 | destinations = ["sirius"] 183 | if input(TEXT["BLACK_HOLE_EXAMINE_QUESTION"]) == "yes": 184 | if engines and copilot: 185 | print(TEXT["BLACK_HOLE_COPILOT_SAVES_YOU"]) 186 | planet = "oracle" 187 | else: 188 | print(TEXT["BLACK_HOLE_CRUNCHED"]) 189 | return 190 | 191 | if planet == "oracle": 192 | print(TEXT["ORACLE_QUESTION"]) 193 | answer = input() 194 | if answer == "42": 195 | print(TEXT["ORACLE_CORRECT"]) 196 | crystal_found = True 197 | else: 198 | print(TEXT["ORACLE_INCORRECT"]) 199 | engines = False 200 | 201 | destinations = ["earth"] 202 | 203 | # display hyperjump destinations 204 | print("\nWhere do you want to travel?") 205 | position = 1 206 | for d in destinations: 207 | print(f"[{position}] {d}") 208 | position += 1 209 | 210 | # choose the next planet 211 | travel_to = None 212 | while travel_to not in destinations: 213 | text = input() 214 | try: 215 | index = int(text) 216 | travel_to = destinations[index - 1] 217 | except ValueError: 218 | print("please enter a number") 219 | except IndexError: 220 | print(f"please enter 1-{len(destinations)}") 221 | planet = travel_to 222 | 223 | print(TEXT["END_CREDITS"]) 224 | 225 | 226 | if __name__ == "__main__": 227 | travel() 228 | -------------------------------------------------------------------------------- /advanced/test_space_game.py: -------------------------------------------------------------------------------- 1 | import io 2 | import pytest 3 | 4 | from space_game import travel 5 | 6 | 7 | # the actual solution to the game 8 | SOLUTION = [ 9 | "2", 10 | "2", # go to sirius and win quiz 11 | "1", 12 | "1", 13 | "yes", # go to centauri and buy GPU drive 14 | "2", 15 | "yes", # hire copilot on orion 16 | "2", 17 | "3", 18 | "yes", # jump into black hole 19 | "42", 20 | "1", # get crystal 21 | ] 22 | 23 | DIE_BY_BLACK_HOLE = [ 24 | "2", 25 | "2", # go to sirius and win quiz 26 | "1", 27 | "1", 28 | "yes", # go to centauri and buy GPU drive 29 | "2", 30 | "no", # hire copilot on orion 31 | "2", 32 | "3", 33 | "yes", # jump into black hole 34 | ] 35 | 36 | # text sniplets that should appear literally in the output 37 | PHRASES = [ 38 | "The stars are waiting for you", 39 | "Betelgeuse", 40 | "credits", 41 | "tech-savvy copilot", 42 | "buy", 43 | "Do you want to hire them", 44 | "Black Hole", 45 | "stupid idea", 46 | "Return to your people in peace", 47 | "THE END", 48 | ] 49 | 50 | 51 | @pytest.fixture 52 | def solution_input(): 53 | """helper function to hijack the keyboard for testing""" 54 | return io.StringIO("\n".join(SOLUTION)) 55 | 56 | 57 | def test_travel(monkeypatch, solution_input): 58 | """game finishes""" 59 | monkeypatch.setattr("sys.stdin", solution_input) 60 | travel() 61 | 62 | 63 | def test_output(monkeypatch, capsys, solution_input): 64 | """text output is not empty""" 65 | monkeypatch.setattr("sys.stdin", solution_input) 66 | 67 | travel() 68 | 69 | captured = capsys.readouterr() 70 | assert len(captured.out) > 0 71 | 72 | 73 | def test_die(monkeypatch, capsys): 74 | """player dies""" 75 | monkeypatch.setattr("sys.stdin", io.StringIO("\n".join(DIE_BY_BLACK_HOLE))) 76 | 77 | travel() 78 | 79 | captured = capsys.readouterr() 80 | assert "crunches" in captured.out 81 | 82 | 83 | @pytest.mark.parametrize("phrase", PHRASES) 84 | def test_output_phrases(monkeypatch, capsys, solution_input, phrase): 85 | """check for some key phrases in the output""" 86 | monkeypatch.setattr("sys.stdin", solution_input) 87 | 88 | travel() 89 | 90 | captured = capsys.readouterr() 91 | assert phrase in captured.out 92 | -------------------------------------------------------------------------------- /solution/01-extract-module/space_game.py: -------------------------------------------------------------------------------- 1 | """ 2 | Space Travel Game 3 | 4 | A simple text adventure written for a refactoring tutorial. 5 | """ 6 | 7 | from text_en import TEXT 8 | 9 | 10 | def travel(): 11 | 12 | print(TEXT["OPENING_MESSAGE"]) 13 | 14 | planet = "earth" 15 | engines = False 16 | copilot = False 17 | credits = False 18 | game_end = False 19 | 20 | while not game_end: 21 | 22 | # display inventory 23 | print("-" * 79) 24 | inventory = "\nYou have: " 25 | inventory += "plenty of credits, " if credits else "" 26 | inventory += "a hyperdrive, " if engines else "" 27 | inventory += "a skilled copilot, " if copilot else "" 28 | if inventory.endswith(", "): 29 | print(inventory.strip(", ")) 30 | 31 | # 32 | # interaction with planets 33 | # 34 | if planet == "earth": 35 | destinations = ["centauri", "sirius"] 36 | print(TEXT["EARTH_DESCRIPTION"]) 37 | 38 | if planet == "centauri": 39 | print(TEXT["CENTAURI_DESCRIPTION"]) 40 | destinations = ["earth", "orion"] 41 | 42 | if not engines: 43 | print(TEXT["HYPERDRIVE_SHOPPING_QUESTION"]) 44 | if input() == "yes": 45 | if credits: 46 | engines = True 47 | else: 48 | print(TEXT["HYPERDRIVE_TOO_EXPENSIVE"]) 49 | 50 | if planet == "sirius": 51 | print(TEXT["SIRIUS_DESCRIPTION"]) 52 | destinations = ["orion", "earth", "black_hole"] 53 | 54 | if not credits: 55 | print(TEXT["SIRIUS_QUIZ_QUESTION"]) 56 | answer = input() 57 | if answer == "2": 58 | print(TEXT["SIRIUS_QUIZ_CORRECT"]) 59 | credits = True 60 | else: 61 | print(TEXT["SIRIUS_QUIZ_INCORRECT"]) 62 | 63 | if planet == "orion": 64 | destinations = ["centauri", "sirius"] 65 | if not copilot: 66 | print(TEXT["ORION_DESCRIPTION"]) 67 | print(TEXT["ORION_HIRE_COPILOT_QUESTION"]) 68 | if input() == "42": 69 | print(TEXT["COPILOT_QUESTION_CORRECT"]) 70 | copilot = True 71 | else: 72 | print(TEXT["COPILOT_QUESTION_INCORRECT"]) 73 | else: 74 | print(TEXT["ORION_DESCRIPTION"]) 75 | 76 | if planet == "black_hole": 77 | print(TEXT["BLACK_HOLE_DESCRIPTION"]) 78 | destinations = ["sirius"] 79 | if input() == "yes": 80 | if engines and copilot: 81 | print(TEXT["BLACK_HOLE_COPILOT_SAVES_YOU"]) 82 | game_end = True 83 | else: 84 | print(TEXT["BLACK_HOLE_CRUNCHED"]) 85 | return 86 | 87 | if not game_end: 88 | # select next planet 89 | print("\nWhere do you want to travel?") 90 | position = 1 91 | for d in destinations: 92 | print(f"[{position}] {d}") 93 | position += 1 94 | 95 | choice = input() 96 | planet = destinations[int(choice) - 1] 97 | 98 | print(TEXT["END_CREDITS"]) 99 | 100 | 101 | if __name__ == "__main__": 102 | travel() 103 | -------------------------------------------------------------------------------- /solution/01-extract-module/test_space_game.py: -------------------------------------------------------------------------------- 1 | import io 2 | import pytest 3 | 4 | from space_game import travel 5 | 6 | 7 | # the actual solution to the game 8 | SOLUTION = [ 9 | "2", 10 | "2", # go to sirius and win quiz 11 | "1", 12 | "42", # hire copilot on orion 13 | "1", 14 | "yes", # go to centauri and buy GPU drive 15 | "2", 16 | "2", 17 | "3", 18 | "yes", # jump into black hole 19 | ] 20 | 21 | DEATH_BY_BLACK_HOLE = [ 22 | "2", 23 | "2", # go to sirius and win quiz 24 | "1", 25 | "41", # hire copilot on orion 26 | "1", 27 | "yes", # go to centauri and buy GPU drive 28 | "1", 29 | "2", 30 | "3", 31 | "yes", # jump into black hole 32 | ] 33 | 34 | # text sniplets that should appear literally in the output 35 | PHRASES = [ 36 | "The stars are waiting for you", 37 | "Betelgeuse", 38 | "credits", 39 | "tech-savvy native", 40 | "copilot", 41 | "buy", 42 | "life, the universe and everything", 43 | "Black Hole", 44 | "stupid idea", 45 | "wonders beyond description", 46 | "THE END", 47 | ] 48 | 49 | 50 | @pytest.fixture 51 | def solution_input(): 52 | """helper function to hijack the keyboard for testing""" 53 | return io.StringIO("\n".join(SOLUTION)) 54 | 55 | 56 | def test_travel(monkeypatch, solution_input): 57 | """game finishes""" 58 | monkeypatch.setattr("sys.stdin", solution_input) 59 | travel() 60 | 61 | 62 | def test_output(monkeypatch, capsys, solution_input): 63 | """text output is not empty""" 64 | monkeypatch.setattr("sys.stdin", solution_input) 65 | 66 | travel() 67 | 68 | captured = capsys.readouterr() 69 | assert len(captured.out) > 0 70 | 71 | 72 | def test_die(monkeypatch, capsys): 73 | """player dies""" 74 | monkeypatch.setattr("sys.stdin", io.StringIO("\n".join(DEATH_BY_BLACK_HOLE))) 75 | 76 | travel() 77 | 78 | captured = capsys.readouterr() 79 | assert "grain of dust" in captured.out 80 | assert " wonders beyond description" not in captured.out 81 | 82 | 83 | @pytest.mark.parametrize("phrase", PHRASES) 84 | def test_output_phrases(monkeypatch, capsys, solution_input, phrase): 85 | """check for some key phrases in the output""" 86 | monkeypatch.setattr("sys.stdin", solution_input) 87 | 88 | travel() 89 | 90 | captured = capsys.readouterr() 91 | assert phrase in captured.out 92 | -------------------------------------------------------------------------------- /solution/01-extract-module/text_en.py: -------------------------------------------------------------------------------- 1 | TEXT = { 2 | "OPENING_MESSAGE": """ 3 | ------------------------------------------------------------------------------- 4 | 5 | You and your trusted spaceship set out to look for 6 | fame, wisdom, and adventure. The stars are waiting for you. 7 | """, 8 | "EARTH_DESCRIPTION": "\nYou are on Earth. Beautiful is better than ugly.", 9 | "CENTAURI_DESCRIPTION": "\nYou are on Alpha Centauri. All creatures are welcome here.", 10 | "HYPERDRIVE_SHOPPING_QUESTION": """There is a brand new hyperdrive with a built-in GPU for sale. 11 | 12 | Would you like to buy one [yes/no]""", 13 | "HYPERDRIVE_TOO_EXPENSIVE": """ 14 | You cannot afford it. The GPU is too expensive.""", 15 | "SIRIUS_DESCRIPTION": """ 16 | You are on Sirius. The system is full of media companies and content delivery networks.""", 17 | "SIRIUS_QUIZ_QUESTION": """You manage to get a place in *Stellar* - the greatest quiz show in the universe. 18 | Here is your question: 19 | 20 | Which star do you find on the shoulder of Orion? 21 | 22 | [1] Altair 23 | [2] Betelgeuse 24 | [3] Aldebaran 25 | [4] Andromeda 26 | """, 27 | "SIRIUS_QUIZ_CORRECT": """ 28 | *Correct!!!* You win a ton or credits. 29 | """, 30 | "SIRIUS_QUIZ_INCORRECT": """ 31 | Sorry, this was the wrong answer. Don't take it too sirius. 32 | Better luck next time. 33 | """, 34 | "ORION_DESCRIPTION": """ 35 | You are on Orion. An icy world inhabited by furry sentients.""", 36 | "ORION_HIRE_COPILOT_QUESTION": """A tech-savvy native admires your spaceship. 37 | They promise to join as a copilot if you can answer a question: 38 | 39 | What is the answer to question of life, the universe and everything? 40 | 41 | What do you answer?""", 42 | "COPILOT_QUESTION_CORRECT": """ 43 | Your new copilot jumps on board and immediately starts 44 | configuring new docker containers. 45 | """, 46 | "COPILOT_QUESTION_INCORRECT": """ 47 | Sorry, that's not it. Try again later. 48 | """, 49 | "BLACK_HOLE_DESCRIPTION": """ 50 | You are close to Black Hole #0997. Maybe coming here was a really stupid idea. 51 | Do you want to examine the black hole closer? [yes/no] 52 | """, 53 | "BLACK_HOLE_CRUNCHED": """ 54 | The black hole condenses your spaceship into a grain of dust. 55 | 56 | THE END 57 | """, 58 | "BLACK_HOLE_COPILOT_SAVES_YOU": """ 59 | On the rim of the black hole your copilot blurts out: 60 | 61 | Turn left! 62 | 63 | You ignite the next-gen hyperdrive, creating a time-space anomaly. 64 | You travel through other dimensions and experience wonders beyond description. 65 | """, 66 | "END_CREDITS": """ 67 | THE END 68 | """, 69 | } 70 | -------------------------------------------------------------------------------- /solution/02-extract-function/space_game.py: -------------------------------------------------------------------------------- 1 | """ 2 | Space Travel Game 3 | 4 | A simple text adventure written for a refactoring tutorial. 5 | """ 6 | 7 | from text_en import TEXT 8 | 9 | 10 | def display_inventory(credits, engines, copilot): 11 | print("-" * 79) 12 | inventory = "\nYou have: " 13 | inventory += "plenty of credits, " if credits else "" 14 | inventory += "a hyperdrive, " if engines else "" 15 | inventory += "a skilled copilot, " if copilot else "" 16 | if inventory.endswith(", "): 17 | print(inventory.strip(", ")) 18 | 19 | 20 | def select_planet(destinations): 21 | print("\nWhere do you want to travel?") 22 | for i, d in enumerate(destinations, 1): 23 | print(f"[{i}] {d}") 24 | 25 | choice = input() 26 | return destinations[int(choice) - 1] 27 | 28 | 29 | def travel(): 30 | 31 | print(TEXT["OPENING_MESSAGE"]) 32 | 33 | planet = "earth" 34 | engines = False 35 | copilot = False 36 | credits = False 37 | game_end = False 38 | 39 | while not game_end: 40 | display_inventory(credits, engines, copilot) 41 | 42 | # 43 | # interaction with planets 44 | # 45 | if planet == "earth": 46 | destinations = ["centauri", "sirius"] 47 | print(TEXT["EARTH_DESCRIPTION"]) 48 | 49 | if planet == "centauri": 50 | print(TEXT["CENTAURI_DESCRIPTION"]) 51 | destinations = ["earth", "orion"] 52 | 53 | if not engines: 54 | print(TEXT["HYPERDRIVE_SHOPPING_QUESTION"]) 55 | if input() == "yes": 56 | if credits: 57 | engines = True 58 | else: 59 | print(TEXT["HYPERDRIVE_TOO_EXPENSIVE"]) 60 | 61 | if planet == "sirius": 62 | print(TEXT["SIRIUS_DESCRIPTION"]) 63 | destinations = ["orion", "earth", "black_hole"] 64 | 65 | if not credits: 66 | print(TEXT["SIRIUS_QUIZ_QUESTION"]) 67 | answer = input() 68 | if answer == "2": 69 | print(TEXT["SIRIUS_QUIZ_CORRECT"]) 70 | credits = True 71 | else: 72 | print(TEXT["SIRIUS_QUIZ_INCORRECT"]) 73 | 74 | if planet == "orion": 75 | destinations = ["centauri", "sirius"] 76 | if not copilot: 77 | print(TEXT["ORION_DESCRIPTION"]) 78 | print(TEXT["ORION_HIRE_COPILOT_QUESTION"]) 79 | if input() == "42": 80 | print(TEXT["COPILOT_QUESTION_CORRECT"]) 81 | copilot = True 82 | else: 83 | print(TEXT["COPILOT_QUESTION_INCORRECT"]) 84 | else: 85 | print(TEXT["ORION_DESCRIPTION"]) 86 | 87 | if planet == "black_hole": 88 | print(TEXT["BLACK_HOLE_DESCRIPTION"]) 89 | destinations = ["sirius"] 90 | if input() == "yes": 91 | if engines and copilot: 92 | print(TEXT["BLACK_HOLE_COPILOT_SAVES_YOU"]) 93 | game_end = True 94 | else: 95 | print(TEXT["BLACK_HOLE_CRUNCHED"]) 96 | return 97 | 98 | if not game_end: 99 | planet = select_planet(destinations) 100 | 101 | print(TEXT["END_CREDITS"]) 102 | 103 | 104 | if __name__ == "__main__": 105 | travel() 106 | -------------------------------------------------------------------------------- /solution/02-extract-function/test_space_game.py: -------------------------------------------------------------------------------- 1 | import io 2 | import pytest 3 | 4 | from space_game import travel 5 | 6 | 7 | # the actual solution to the game 8 | SOLUTION = [ 9 | "2", 10 | "2", # go to sirius and win quiz 11 | "1", 12 | "42", # hire copilot on orion 13 | "1", 14 | "yes", # go to centauri and buy GPU drive 15 | "2", 16 | "2", 17 | "3", 18 | "yes", # jump into black hole 19 | ] 20 | 21 | DEATH_BY_BLACK_HOLE = [ 22 | "2", 23 | "2", # go to sirius and win quiz 24 | "1", 25 | "41", # hire copilot on orion 26 | "1", 27 | "yes", # go to centauri and buy GPU drive 28 | "1", 29 | "2", 30 | "3", 31 | "yes", # jump into black hole 32 | ] 33 | 34 | # text sniplets that should appear literally in the output 35 | PHRASES = [ 36 | "The stars are waiting for you", 37 | "Betelgeuse", 38 | "credits", 39 | "tech-savvy native", 40 | "copilot", 41 | "buy", 42 | "life, the universe and everything", 43 | "Black Hole", 44 | "stupid idea", 45 | "wonders beyond description", 46 | "THE END", 47 | ] 48 | 49 | 50 | @pytest.fixture 51 | def solution_input(): 52 | """helper function to hijack the keyboard for testing""" 53 | return io.StringIO("\n".join(SOLUTION)) 54 | 55 | 56 | def test_travel(monkeypatch, solution_input): 57 | """game finishes""" 58 | monkeypatch.setattr("sys.stdin", solution_input) 59 | travel() 60 | 61 | 62 | def test_output(monkeypatch, capsys, solution_input): 63 | """text output is not empty""" 64 | monkeypatch.setattr("sys.stdin", solution_input) 65 | 66 | travel() 67 | 68 | captured = capsys.readouterr() 69 | assert len(captured.out) > 0 70 | 71 | 72 | def test_die(monkeypatch, capsys): 73 | """player dies""" 74 | monkeypatch.setattr("sys.stdin", io.StringIO("\n".join(DEATH_BY_BLACK_HOLE))) 75 | 76 | travel() 77 | 78 | captured = capsys.readouterr() 79 | assert "grain of dust" in captured.out 80 | assert " wonders beyond description" not in captured.out 81 | 82 | 83 | @pytest.mark.parametrize("phrase", PHRASES) 84 | def test_output_phrases(monkeypatch, capsys, solution_input, phrase): 85 | """check for some key phrases in the output""" 86 | monkeypatch.setattr("sys.stdin", solution_input) 87 | 88 | travel() 89 | 90 | captured = capsys.readouterr() 91 | assert phrase in captured.out 92 | -------------------------------------------------------------------------------- /solution/02-extract-function/text_en.py: -------------------------------------------------------------------------------- 1 | TEXT = { 2 | "OPENING_MESSAGE": """ 3 | ------------------------------------------------------------------------------- 4 | 5 | You and your trusted spaceship set out to look for 6 | fame, wisdom, and adventure. The stars are waiting for you. 7 | """, 8 | "EARTH_DESCRIPTION": "\nYou are on Earth. Beautiful is better than ugly.", 9 | "CENTAURI_DESCRIPTION": "\nYou are on Alpha Centauri. All creatures are welcome here.", 10 | "HYPERDRIVE_SHOPPING_QUESTION": """There is a brand new hyperdrive with a built-in GPU for sale. 11 | 12 | Would you like to buy one [yes/no]""", 13 | "HYPERDRIVE_TOO_EXPENSIVE": """ 14 | You cannot afford it. The GPU is too expensive.""", 15 | "SIRIUS_DESCRIPTION": """ 16 | You are on Sirius. The system is full of media companies and content delivery networks.""", 17 | "SIRIUS_QUIZ_QUESTION": """You manage to get a place in *Stellar* - the greatest quiz show in the universe. 18 | Here is your question: 19 | 20 | Which star do you find on the shoulder of Orion? 21 | 22 | [1] Altair 23 | [2] Betelgeuse 24 | [3] Aldebaran 25 | [4] Andromeda 26 | """, 27 | "SIRIUS_QUIZ_CORRECT": """ 28 | *Correct!!!* You win a ton or credits. 29 | """, 30 | "SIRIUS_QUIZ_INCORRECT": """ 31 | Sorry, this was the wrong answer. Don't take it too sirius. 32 | Better luck next time. 33 | """, 34 | "ORION_DESCRIPTION": """ 35 | You are on Orion. An icy world inhabited by furry sentients.""", 36 | "ORION_HIRE_COPILOT_QUESTION": """A tech-savvy native admires your spaceship. 37 | They promise to join as a copilot if you can answer a question: 38 | 39 | What is the answer to question of life, the universe and everything? 40 | 41 | What do you answer?""", 42 | "COPILOT_QUESTION_CORRECT": """ 43 | Your new copilot jumps on board and immediately starts 44 | configuring new docker containers. 45 | """, 46 | "COPILOT_QUESTION_INCORRECT": """ 47 | Sorry, that's not it. Try again later. 48 | """, 49 | "BLACK_HOLE_DESCRIPTION": """ 50 | You are close to Black Hole #0997. Maybe coming here was a really stupid idea. 51 | Do you want to examine the black hole closer? [yes/no] 52 | """, 53 | "BLACK_HOLE_CRUNCHED": """ 54 | The black hole condenses your spaceship into a grain of dust. 55 | 56 | THE END 57 | """, 58 | "BLACK_HOLE_COPILOT_SAVES_YOU": """ 59 | On the rim of the black hole your copilot blurts out: 60 | 61 | Turn left! 62 | 63 | You ignite the next-gen hyperdrive, creating a time-space anomaly. 64 | You travel through other dimensions and experience wonders beyond description. 65 | """, 66 | "END_CREDITS": """ 67 | THE END 68 | """, 69 | } 70 | -------------------------------------------------------------------------------- /solution/03-extract-and-modify/space_game.py: -------------------------------------------------------------------------------- 1 | """ 2 | Space Travel Game 3 | 4 | A simple text adventure written for a refactoring tutorial. 5 | """ 6 | 7 | from text_en import TEXT 8 | 9 | 10 | def display_inventory(credits, engines, copilot): 11 | print("-" * 79) 12 | inventory = "\nYou have: " 13 | inventory += "plenty of credits, " if credits else "" 14 | inventory += "a hyperdrive, " if engines else "" 15 | inventory += "a skilled copilot, " if copilot else "" 16 | if inventory.endswith(", "): 17 | print(inventory.strip(", ")) 18 | 19 | 20 | def select_planet(destinations): 21 | print("\nWhere do you want to travel?") 22 | for i, d in enumerate(destinations, 1): 23 | print(f"[{i}] {d}") 24 | 25 | choice = input() 26 | return destinations[int(choice) - 1] 27 | 28 | 29 | def visit_planet(planet, engines, copilot, credits, game_end): 30 | if planet == "earth": 31 | destinations = ["centauri", "sirius"] 32 | print(TEXT["EARTH_DESCRIPTION"]) 33 | 34 | if planet == "centauri": 35 | print(TEXT["CENTAURI_DESCRIPTION"]) 36 | destinations = ["earth", "orion"] 37 | 38 | if not engines: 39 | print(TEXT["HYPERDRIVE_SHOPPING_QUESTION"]) 40 | if input() == "yes": 41 | if credits: 42 | engines = True 43 | else: 44 | print(TEXT["HYPERDRIVE_TOO_EXPENSIVE"]) 45 | 46 | if planet == "sirius": 47 | print(TEXT["SIRIUS_DESCRIPTION"]) 48 | destinations = ["orion", "earth", "black_hole"] 49 | 50 | if not credits: 51 | print(TEXT["SIRIUS_QUIZ_QUESTION"]) 52 | answer = input() 53 | if answer == "2": 54 | print(TEXT["SIRIUS_QUIZ_CORRECT"]) 55 | credits = True 56 | else: 57 | print(TEXT["SIRIUS_QUIZ_INCORRECT"]) 58 | 59 | if planet == "orion": 60 | destinations = ["centauri", "sirius"] 61 | if not copilot: 62 | print(TEXT["ORION_DESCRIPTION"]) 63 | print(TEXT["ORION_HIRE_COPILOT_QUESTION"]) 64 | if input() == "42": 65 | print(TEXT["COPILOT_QUESTION_CORRECT"]) 66 | copilot = True 67 | else: 68 | print(TEXT["COPILOT_QUESTION_INCORRECT"]) 69 | else: 70 | print(TEXT["ORION_DESCRIPTION"]) 71 | 72 | if planet == "black_hole": 73 | print(TEXT["BLACK_HOLE_DESCRIPTION"]) 74 | destinations = ["sirius"] 75 | if input() == "yes": 76 | if engines and copilot: 77 | print(TEXT["BLACK_HOLE_COPILOT_SAVES_YOU"]) 78 | print(TEXT["END_CREDITS"]) 79 | else: 80 | print(TEXT["BLACK_HOLE_CRUNCHED"]) 81 | game_end = True 82 | 83 | return destinations, engines, copilot, credits, game_end 84 | 85 | 86 | def travel(): 87 | 88 | print(TEXT["OPENING_MESSAGE"]) 89 | 90 | planet = "earth" 91 | engines = False 92 | copilot = False 93 | credits = False 94 | game_end = False 95 | 96 | while not game_end: 97 | display_inventory(credits, engines, copilot) 98 | destinations, engines, copilot, credits, game_end = visit_planet(planet, engines, copilot, credits, game_end) 99 | if not game_end: 100 | planet = select_planet(destinations) 101 | 102 | 103 | if __name__ == "__main__": 104 | travel() 105 | -------------------------------------------------------------------------------- /solution/03-extract-and-modify/test_space_game.py: -------------------------------------------------------------------------------- 1 | import io 2 | import pytest 3 | 4 | from space_game import travel 5 | 6 | 7 | # the actual solution to the game 8 | SOLUTION = [ 9 | "2", 10 | "2", # go to sirius and win quiz 11 | "1", 12 | "42", # hire copilot on orion 13 | "1", 14 | "yes", # go to centauri and buy GPU drive 15 | "2", 16 | "2", 17 | "3", 18 | "yes", # jump into black hole 19 | ] 20 | 21 | DEATH_BY_BLACK_HOLE = [ 22 | "2", 23 | "2", # go to sirius and win quiz 24 | "1", 25 | "41", # hire copilot on orion 26 | "1", 27 | "yes", # go to centauri and buy GPU drive 28 | "1", 29 | "2", 30 | "3", 31 | "yes", # jump into black hole 32 | ] 33 | 34 | # text sniplets that should appear literally in the output 35 | PHRASES = [ 36 | "The stars are waiting for you", 37 | "Betelgeuse", 38 | "credits", 39 | "tech-savvy native", 40 | "copilot", 41 | "buy", 42 | "life, the universe and everything", 43 | "Black Hole", 44 | "stupid idea", 45 | "wonders beyond description", 46 | "THE END", 47 | ] 48 | 49 | 50 | @pytest.fixture 51 | def solution_input(): 52 | """helper function to hijack the keyboard for testing""" 53 | return io.StringIO("\n".join(SOLUTION)) 54 | 55 | 56 | def test_travel(monkeypatch, solution_input): 57 | """game finishes""" 58 | monkeypatch.setattr("sys.stdin", solution_input) 59 | travel() 60 | 61 | 62 | def test_output(monkeypatch, capsys, solution_input): 63 | """text output is not empty""" 64 | monkeypatch.setattr("sys.stdin", solution_input) 65 | 66 | travel() 67 | 68 | captured = capsys.readouterr() 69 | assert len(captured.out) > 0 70 | 71 | 72 | def test_die(monkeypatch, capsys): 73 | """player dies""" 74 | monkeypatch.setattr("sys.stdin", io.StringIO("\n".join(DEATH_BY_BLACK_HOLE))) 75 | 76 | travel() 77 | 78 | captured = capsys.readouterr() 79 | assert "grain of dust" in captured.out 80 | assert " wonders beyond description" not in captured.out 81 | 82 | 83 | @pytest.mark.parametrize("phrase", PHRASES) 84 | def test_output_phrases(monkeypatch, capsys, solution_input, phrase): 85 | """check for some key phrases in the output""" 86 | monkeypatch.setattr("sys.stdin", solution_input) 87 | 88 | travel() 89 | 90 | captured = capsys.readouterr() 91 | assert phrase in captured.out 92 | -------------------------------------------------------------------------------- /solution/03-extract-and-modify/text_en.py: -------------------------------------------------------------------------------- 1 | TEXT = { 2 | "OPENING_MESSAGE": """ 3 | ------------------------------------------------------------------------------- 4 | 5 | You and your trusted spaceship set out to look for 6 | fame, wisdom, and adventure. The stars are waiting for you. 7 | """, 8 | "EARTH_DESCRIPTION": "\nYou are on Earth. Beautiful is better than ugly.", 9 | "CENTAURI_DESCRIPTION": "\nYou are on Alpha Centauri. All creatures are welcome here.", 10 | "HYPERDRIVE_SHOPPING_QUESTION": """There is a brand new hyperdrive with a built-in GPU for sale. 11 | 12 | Would you like to buy one [yes/no]""", 13 | "HYPERDRIVE_TOO_EXPENSIVE": """ 14 | You cannot afford it. The GPU is too expensive.""", 15 | "SIRIUS_DESCRIPTION": """ 16 | You are on Sirius. The system is full of media companies and content delivery networks.""", 17 | "SIRIUS_QUIZ_QUESTION": """You manage to get a place in *Stellar* - the greatest quiz show in the universe. 18 | Here is your question: 19 | 20 | Which star do you find on the shoulder of Orion? 21 | 22 | [1] Altair 23 | [2] Betelgeuse 24 | [3] Aldebaran 25 | [4] Andromeda 26 | """, 27 | "SIRIUS_QUIZ_CORRECT": """ 28 | *Correct!!!* You win a ton or credits. 29 | """, 30 | "SIRIUS_QUIZ_INCORRECT": """ 31 | Sorry, this was the wrong answer. Don't take it too sirius. 32 | Better luck next time. 33 | """, 34 | "ORION_DESCRIPTION": """ 35 | You are on Orion. An icy world inhabited by furry sentients.""", 36 | "ORION_HIRE_COPILOT_QUESTION": """A tech-savvy native admires your spaceship. 37 | They promise to join as a copilot if you can answer a question: 38 | 39 | What is the answer to question of life, the universe and everything? 40 | 41 | What do you answer?""", 42 | "COPILOT_QUESTION_CORRECT": """ 43 | Your new copilot jumps on board and immediately starts 44 | configuring new docker containers. 45 | """, 46 | "COPILOT_QUESTION_INCORRECT": """ 47 | Sorry, that's not it. Try again later. 48 | """, 49 | "BLACK_HOLE_DESCRIPTION": """ 50 | You are close to Black Hole #0997. Maybe coming here was a really stupid idea. 51 | Do you want to examine the black hole closer? [yes/no] 52 | """, 53 | "BLACK_HOLE_CRUNCHED": """ 54 | The black hole condenses your spaceship into a grain of dust. 55 | 56 | THE END 57 | """, 58 | "BLACK_HOLE_COPILOT_SAVES_YOU": """ 59 | On the rim of the black hole your copilot blurts out: 60 | 61 | Turn left! 62 | 63 | You ignite the next-gen hyperdrive, creating a time-space anomaly. 64 | You travel through other dimensions and experience wonders beyond description. 65 | """, 66 | "END_CREDITS": """ 67 | THE END 68 | """, 69 | } 70 | -------------------------------------------------------------------------------- /solution/04-extract-data-structure/space_game.py: -------------------------------------------------------------------------------- 1 | """ 2 | Space Travel Game 3 | 4 | A simple text adventure written for a refactoring tutorial. 5 | """ 6 | 7 | from text_en import TEXT 8 | 9 | 10 | credits, engines, copilot, game_end = range(4) 11 | 12 | 13 | def display_inventory(flags): 14 | print("-" * 79) 15 | inventory = "\nYou have: " 16 | inventory += "plenty of credits, " if credits in flags else "" 17 | inventory += "a hyperdrive, " if engines in flags else "" 18 | inventory += "a skilled copilot, " if copilot in flags else "" 19 | if inventory.endswith(", "): 20 | print(inventory.strip(", ")) 21 | 22 | 23 | def select_planet(destinations): 24 | print("\nWhere do you want to travel?") 25 | for i, d in enumerate(destinations, 1): 26 | print(f"[{i}] {d}") 27 | 28 | choice = input() 29 | return destinations[int(choice) - 1] 30 | 31 | 32 | def buy_engine(flags): 33 | if engines not in flags: 34 | print(TEXT["HYPERDRIVE_SHOPPING_QUESTION"]) 35 | if input() == "yes": 36 | if credits in flags: 37 | flags.add(engines) 38 | else: 39 | print(TEXT["HYPERDRIVE_TOO_EXPENSIVE"]) 40 | 41 | 42 | def stellar_quiz(flags): 43 | if credits not in flags: 44 | print(TEXT["SIRIUS_QUIZ_QUESTION"]) 45 | answer = input() 46 | if answer == "2": 47 | print(TEXT["SIRIUS_QUIZ_CORRECT"]) 48 | flags.add(credits) 49 | else: 50 | print(TEXT["SIRIUS_QUIZ_INCORRECT"]) 51 | 52 | 53 | def hire_copilot(flags): 54 | if copilot not in flags: 55 | print(TEXT["ORION_HIRE_COPILOT_QUESTION"]) 56 | if input() == "42": 57 | print(TEXT["COPILOT_QUESTION_CORRECT"]) 58 | flags.add(copilot) 59 | else: 60 | print(TEXT["COPILOT_QUESTION_INCORRECT"]) 61 | 62 | 63 | def black_hole(flags): 64 | if input() == "yes": 65 | if engines in flags and copilot in flags: 66 | print(TEXT["BLACK_HOLE_COPILOT_SAVES_YOU"]) 67 | print(TEXT["END_CREDITS"]) 68 | else: 69 | print(TEXT["BLACK_HOLE_CRUNCHED"]) 70 | flags.add(game_end) 71 | 72 | 73 | STARMAP = { 74 | 'earth': ["centauri", "sirius"], 75 | 'centauri': ["earth", "orion"], 76 | 'sirius': ["orion", "earth", "black_hole"], 77 | 'orion': ["centauri", "sirius"], 78 | 'black_hole': ["sirius"] 79 | } 80 | 81 | def visit_planet(planet, flags): 82 | key = planet.upper() + '_DESCRIPTION' 83 | print(TEXT[key]) 84 | 85 | if planet == "centauri": 86 | buy_engine(flags) 87 | 88 | if planet == "sirius": 89 | stellar_quiz(flags) 90 | 91 | if planet == "orion": 92 | hire_copilot(flags) 93 | 94 | if planet == "black_hole": 95 | black_hole(flags) 96 | 97 | return STARMAP[planet] 98 | 99 | 100 | def travel(): 101 | 102 | planet = "earth" 103 | flags = set() 104 | 105 | print(TEXT["OPENING_MESSAGE"]) 106 | destinations = visit_planet(planet, flags) 107 | 108 | while game_end not in flags: 109 | planet = select_planet(destinations) 110 | display_inventory(flags) 111 | destinations = visit_planet(planet, flags) 112 | 113 | 114 | if __name__ == "__main__": 115 | travel() 116 | -------------------------------------------------------------------------------- /solution/04-extract-data-structure/test_space_game.py: -------------------------------------------------------------------------------- 1 | import io 2 | import pytest 3 | 4 | from space_game import travel 5 | 6 | 7 | # the actual solution to the game 8 | SOLUTION = [ 9 | "2", 10 | "2", # go to sirius and win quiz 11 | "1", 12 | "42", # hire copilot on orion 13 | "1", 14 | "yes", # go to centauri and buy GPU drive 15 | "2", 16 | "2", 17 | "3", 18 | "yes", # jump into black hole 19 | ] 20 | 21 | DEATH_BY_BLACK_HOLE = [ 22 | "2", 23 | "2", # go to sirius and win quiz 24 | "1", 25 | "41", # hire copilot on orion 26 | "1", 27 | "yes", # go to centauri and buy GPU drive 28 | "1", 29 | "2", 30 | "3", 31 | "yes", # jump into black hole 32 | ] 33 | 34 | # text sniplets that should appear literally in the output 35 | PHRASES = [ 36 | "The stars are waiting for you", 37 | "Betelgeuse", 38 | "credits", 39 | "tech-savvy native", 40 | "copilot", 41 | "buy", 42 | "life, the universe and everything", 43 | "Black Hole", 44 | "stupid idea", 45 | "wonders beyond description", 46 | "THE END", 47 | ] 48 | 49 | 50 | @pytest.fixture 51 | def solution_input(): 52 | """helper function to hijack the keyboard for testing""" 53 | return io.StringIO("\n".join(SOLUTION)) 54 | 55 | 56 | def test_travel(monkeypatch, solution_input): 57 | """game finishes""" 58 | monkeypatch.setattr("sys.stdin", solution_input) 59 | travel() 60 | 61 | 62 | def test_output(monkeypatch, capsys, solution_input): 63 | """text output is not empty""" 64 | monkeypatch.setattr("sys.stdin", solution_input) 65 | 66 | travel() 67 | 68 | captured = capsys.readouterr() 69 | assert len(captured.out) > 0 70 | 71 | 72 | def test_die(monkeypatch, capsys): 73 | """player dies""" 74 | monkeypatch.setattr("sys.stdin", io.StringIO("\n".join(DEATH_BY_BLACK_HOLE))) 75 | 76 | travel() 77 | 78 | captured = capsys.readouterr() 79 | assert "grain of dust" in captured.out 80 | assert " wonders beyond description" not in captured.out 81 | 82 | 83 | @pytest.mark.parametrize("phrase", PHRASES) 84 | def test_output_phrases(monkeypatch, capsys, solution_input, phrase): 85 | """check for some key phrases in the output""" 86 | monkeypatch.setattr("sys.stdin", solution_input) 87 | 88 | travel() 89 | 90 | captured = capsys.readouterr() 91 | assert phrase in captured.out 92 | -------------------------------------------------------------------------------- /solution/04-extract-data-structure/text_en.py: -------------------------------------------------------------------------------- 1 | TEXT = { 2 | "OPENING_MESSAGE": """ 3 | ------------------------------------------------------------------------------- 4 | 5 | You and your trusted spaceship set out to look for 6 | fame, wisdom, and adventure. The stars are waiting for you. 7 | """, 8 | "EARTH_DESCRIPTION": "\nYou are on Earth. Beautiful is better than ugly.", 9 | "CENTAURI_DESCRIPTION": "\nYou are on Alpha Centauri. All creatures are welcome here.", 10 | "HYPERDRIVE_SHOPPING_QUESTION": """There is a brand new hyperdrive with a built-in GPU for sale. 11 | 12 | Would you like to buy one [yes/no]""", 13 | "HYPERDRIVE_TOO_EXPENSIVE": """ 14 | You cannot afford it. The GPU is too expensive.""", 15 | "SIRIUS_DESCRIPTION": """ 16 | You are on Sirius. The system is full of media companies and content delivery networks.""", 17 | "SIRIUS_QUIZ_QUESTION": """You manage to get a place in *Stellar* - the greatest quiz show in the universe. 18 | Here is your question: 19 | 20 | Which star do you find on the shoulder of Orion? 21 | 22 | [1] Altair 23 | [2] Betelgeuse 24 | [3] Aldebaran 25 | [4] Andromeda 26 | """, 27 | "SIRIUS_QUIZ_CORRECT": """ 28 | *Correct!!!* You win a ton or credits. 29 | """, 30 | "SIRIUS_QUIZ_INCORRECT": """ 31 | Sorry, this was the wrong answer. Don't take it too sirius. 32 | Better luck next time. 33 | """, 34 | "ORION_DESCRIPTION": """ 35 | You are on Orion. An icy world inhabited by furry sentients.""", 36 | "ORION_HIRE_COPILOT_QUESTION": """A tech-savvy native admires your spaceship. 37 | They promise to join as a copilot if you can answer a question: 38 | 39 | What is the answer to question of life, the universe and everything? 40 | 41 | What do you answer?""", 42 | "COPILOT_QUESTION_CORRECT": """ 43 | Your new copilot jumps on board and immediately starts 44 | configuring new docker containers. 45 | """, 46 | "COPILOT_QUESTION_INCORRECT": """ 47 | Sorry, that's not it. Try again later. 48 | """, 49 | "BLACK_HOLE_DESCRIPTION": """ 50 | You are close to Black Hole #0997. Maybe coming here was a really stupid idea. 51 | Do you want to examine the black hole closer? [yes/no] 52 | """, 53 | "BLACK_HOLE_CRUNCHED": """ 54 | The black hole condenses your spaceship into a grain of dust. 55 | 56 | THE END 57 | """, 58 | "BLACK_HOLE_COPILOT_SAVES_YOU": """ 59 | On the rim of the black hole your copilot blurts out: 60 | 61 | Turn left! 62 | 63 | You ignite the next-gen hyperdrive, creating a time-space anomaly. 64 | You travel through other dimensions and experience wonders beyond description. 65 | """, 66 | "END_CREDITS": """ 67 | THE END 68 | """, 69 | } 70 | -------------------------------------------------------------------------------- /solution/05-extract-class/puzzles.py: -------------------------------------------------------------------------------- 1 | 2 | from text_en import TEXT 3 | 4 | 5 | credits, engines, copilot, game_end = range(4) 6 | 7 | 8 | def buy_engine(flags): 9 | if engines not in flags: 10 | print(TEXT["HYPERDRIVE_SHOPPING_QUESTION"]) 11 | if input() == "yes": 12 | if credits in flags: 13 | flags.add(engines) 14 | else: 15 | print(TEXT["HYPERDRIVE_TOO_EXPENSIVE"]) 16 | 17 | 18 | def stellar_quiz(flags): 19 | if credits not in flags: 20 | print(TEXT["SIRIUS_QUIZ_QUESTION"]) 21 | answer = input() 22 | if answer == "2": 23 | print(TEXT["SIRIUS_QUIZ_CORRECT"]) 24 | flags.add(credits) 25 | else: 26 | print(TEXT["SIRIUS_QUIZ_INCORRECT"]) 27 | 28 | 29 | def hire_copilot(flags): 30 | if copilot not in flags: 31 | print(TEXT["ORION_HIRE_COPILOT_QUESTION"]) 32 | if input() == "42": 33 | print(TEXT["COPILOT_QUESTION_CORRECT"]) 34 | flags.add(copilot) 35 | else: 36 | print(TEXT["COPILOT_QUESTION_INCORRECT"]) 37 | 38 | 39 | def black_hole(flags): 40 | if input() == "yes": 41 | if engines in flags and copilot in flags: 42 | print(TEXT["BLACK_HOLE_COPILOT_SAVES_YOU"]) 43 | print(TEXT["END_CREDITS"]) 44 | else: 45 | print(TEXT["BLACK_HOLE_CRUNCHED"]) 46 | flags.add(game_end) 47 | -------------------------------------------------------------------------------- /solution/05-extract-class/space_game.py: -------------------------------------------------------------------------------- 1 | """ 2 | Space Travel Game 3 | 4 | A simple text adventure written for a refactoring tutorial. 5 | """ 6 | 7 | from text_en import TEXT 8 | from puzzles import buy_engine, hire_copilot, stellar_quiz, black_hole 9 | from puzzles import credits, engines, copilot, game_end 10 | 11 | 12 | def display_inventory(flags): 13 | print("-" * 79) 14 | inventory = "\nYou have: " 15 | inventory += "plenty of credits, " if credits in flags else "" 16 | inventory += "a hyperdrive, " if engines in flags else "" 17 | inventory += "a skilled copilot, " if copilot in flags else "" 18 | if inventory.endswith(", "): 19 | print(inventory.strip(", ")) 20 | 21 | 22 | def select_planet(destinations): 23 | print("\nWhere do you want to travel?") 24 | for i, d in enumerate(destinations, 1): 25 | print(f"[{i}] {d}") 26 | 27 | choice = input() 28 | return PLANETS[destinations[int(choice) - 1]] 29 | 30 | 31 | class Planet: 32 | 33 | def __init__(self, name, connections, puzzle=None): 34 | self.name = name 35 | self.description = TEXT[name.upper() + "_DESCRIPTION"] 36 | self.connections = connections 37 | self.puzzle = puzzle 38 | 39 | def visit(self, flags): 40 | print(self.description) 41 | if self.puzzle: 42 | self.puzzle(flags) 43 | 44 | 45 | PLANETS = {p.name: p for p in [ 46 | Planet('earth', ["centauri", "sirius"]), 47 | Planet('centauri', ["earth", "orion"], buy_engine), 48 | Planet('sirius', ["orion", "earth", "black_hole"], stellar_quiz), 49 | Planet('orion', ["centauri", "sirius"], hire_copilot), 50 | Planet('black_hole', ["sirius"], black_hole) 51 | ]} 52 | 53 | 54 | def travel(): 55 | """main game function""" 56 | planet = PLANETS["earth"] 57 | flags = set() 58 | 59 | print(TEXT["OPENING_MESSAGE"]) 60 | planet.visit(flags) 61 | 62 | while game_end not in flags: 63 | planet = select_planet(planet.connections) 64 | display_inventory(flags) 65 | planet.visit(flags) 66 | 67 | 68 | if __name__ == "__main__": 69 | travel() 70 | -------------------------------------------------------------------------------- /solution/05-extract-class/test_space_game.py: -------------------------------------------------------------------------------- 1 | import io 2 | import pytest 3 | 4 | from space_game import travel 5 | 6 | 7 | # the actual solution to the game 8 | SOLUTION = [ 9 | "2", 10 | "2", # go to sirius and win quiz 11 | "1", 12 | "42", # hire copilot on orion 13 | "1", 14 | "yes", # go to centauri and buy GPU drive 15 | "2", 16 | "2", 17 | "3", 18 | "yes", # jump into black hole 19 | ] 20 | 21 | DEATH_BY_BLACK_HOLE = [ 22 | "2", 23 | "2", # go to sirius and win quiz 24 | "1", 25 | "41", # hire copilot on orion 26 | "1", 27 | "yes", # go to centauri and buy GPU drive 28 | "1", 29 | "2", 30 | "3", 31 | "yes", # jump into black hole 32 | ] 33 | 34 | # text sniplets that should appear literally in the output 35 | PHRASES = [ 36 | "The stars are waiting for you", 37 | "Betelgeuse", 38 | "credits", 39 | "tech-savvy native", 40 | "copilot", 41 | "buy", 42 | "life, the universe and everything", 43 | "Black Hole", 44 | "stupid idea", 45 | "wonders beyond description", 46 | "THE END", 47 | ] 48 | 49 | 50 | @pytest.fixture 51 | def solution_input(): 52 | """helper function to hijack the keyboard for testing""" 53 | return io.StringIO("\n".join(SOLUTION)) 54 | 55 | 56 | def test_travel(monkeypatch, solution_input): 57 | """game finishes""" 58 | monkeypatch.setattr("sys.stdin", solution_input) 59 | travel() 60 | 61 | 62 | def test_output(monkeypatch, capsys, solution_input): 63 | """text output is not empty""" 64 | monkeypatch.setattr("sys.stdin", solution_input) 65 | 66 | travel() 67 | 68 | captured = capsys.readouterr() 69 | assert len(captured.out) > 0 70 | 71 | 72 | def test_die(monkeypatch, capsys): 73 | """player dies""" 74 | monkeypatch.setattr("sys.stdin", io.StringIO("\n".join(DEATH_BY_BLACK_HOLE))) 75 | 76 | travel() 77 | 78 | captured = capsys.readouterr() 79 | assert "grain of dust" in captured.out 80 | assert " wonders beyond description" not in captured.out 81 | 82 | 83 | @pytest.mark.parametrize("phrase", PHRASES) 84 | def test_output_phrases(monkeypatch, capsys, solution_input, phrase): 85 | """check for some key phrases in the output""" 86 | monkeypatch.setattr("sys.stdin", solution_input) 87 | 88 | travel() 89 | 90 | captured = capsys.readouterr() 91 | assert phrase in captured.out 92 | -------------------------------------------------------------------------------- /solution/05-extract-class/text_en.py: -------------------------------------------------------------------------------- 1 | TEXT = { 2 | "OPENING_MESSAGE": """ 3 | ------------------------------------------------------------------------------- 4 | 5 | You and your trusted spaceship set out to look for 6 | fame, wisdom, and adventure. The stars are waiting for you. 7 | """, 8 | "EARTH_DESCRIPTION": "\nYou are on Earth. Beautiful is better than ugly.", 9 | "CENTAURI_DESCRIPTION": "\nYou are on Alpha Centauri. All creatures are welcome here.", 10 | "HYPERDRIVE_SHOPPING_QUESTION": """There is a brand new hyperdrive with a built-in GPU for sale. 11 | 12 | Would you like to buy one [yes/no]""", 13 | "HYPERDRIVE_TOO_EXPENSIVE": """ 14 | You cannot afford it. The GPU is too expensive.""", 15 | "SIRIUS_DESCRIPTION": """ 16 | You are on Sirius. The system is full of media companies and content delivery networks.""", 17 | "SIRIUS_QUIZ_QUESTION": """You manage to get a place in *Stellar* - the greatest quiz show in the universe. 18 | Here is your question: 19 | 20 | Which star do you find on the shoulder of Orion? 21 | 22 | [1] Altair 23 | [2] Betelgeuse 24 | [3] Aldebaran 25 | [4] Andromeda 26 | """, 27 | "SIRIUS_QUIZ_CORRECT": """ 28 | *Correct!!!* You win a ton or credits. 29 | """, 30 | "SIRIUS_QUIZ_INCORRECT": """ 31 | Sorry, this was the wrong answer. Don't take it too sirius. 32 | Better luck next time. 33 | """, 34 | "ORION_DESCRIPTION": """ 35 | You are on Orion. An icy world inhabited by furry sentients.""", 36 | "ORION_HIRE_COPILOT_QUESTION": """A tech-savvy native admires your spaceship. 37 | They promise to join as a copilot if you can answer a question: 38 | 39 | What is the answer to question of life, the universe and everything? 40 | 41 | What do you answer?""", 42 | "COPILOT_QUESTION_CORRECT": """ 43 | Your new copilot jumps on board and immediately starts 44 | configuring new docker containers. 45 | """, 46 | "COPILOT_QUESTION_INCORRECT": """ 47 | Sorry, that's not it. Try again later. 48 | """, 49 | "BLACK_HOLE_DESCRIPTION": """ 50 | You are close to Black Hole #0997. Maybe coming here was a really stupid idea. 51 | Do you want to examine the black hole closer? [yes/no] 52 | """, 53 | "BLACK_HOLE_CRUNCHED": """ 54 | The black hole condenses your spaceship into a grain of dust. 55 | 56 | THE END 57 | """, 58 | "BLACK_HOLE_COPILOT_SAVES_YOU": """ 59 | On the rim of the black hole your copilot blurts out: 60 | 61 | Turn left! 62 | 63 | You ignite the next-gen hyperdrive, creating a time-space anomaly. 64 | You travel through other dimensions and experience wonders beyond description. 65 | """, 66 | "END_CREDITS": """ 67 | THE END 68 | """, 69 | } 70 | -------------------------------------------------------------------------------- /solution/06-another-class/puzzles.py: -------------------------------------------------------------------------------- 1 | 2 | from text_en import TEXT 3 | 4 | 5 | credits, engines, copilot, game_end = range(4) 6 | 7 | 8 | def buy_engine(flags): 9 | if engines not in flags: 10 | print(TEXT["HYPERDRIVE_SHOPPING_QUESTION"]) 11 | if input() == "yes": 12 | if credits in flags: 13 | flags.add(engines) 14 | else: 15 | print(TEXT["HYPERDRIVE_TOO_EXPENSIVE"]) 16 | 17 | 18 | def stellar_quiz(flags): 19 | if credits not in flags: 20 | print(TEXT["SIRIUS_QUIZ_QUESTION"]) 21 | answer = input() 22 | if answer == "2": 23 | print(TEXT["SIRIUS_QUIZ_CORRECT"]) 24 | flags.add(credits) 25 | else: 26 | print(TEXT["SIRIUS_QUIZ_INCORRECT"]) 27 | 28 | 29 | def hire_copilot(flags): 30 | if copilot not in flags: 31 | print(TEXT["ORION_HIRE_COPILOT_QUESTION"]) 32 | if input() == "42": 33 | print(TEXT["COPILOT_QUESTION_CORRECT"]) 34 | flags.add(copilot) 35 | else: 36 | print(TEXT["COPILOT_QUESTION_INCORRECT"]) 37 | 38 | 39 | def black_hole(flags): 40 | if input() == "yes": 41 | if engines in flags and copilot in flags: 42 | print(TEXT["BLACK_HOLE_COPILOT_SAVES_YOU"]) 43 | print(TEXT["END_CREDITS"]) 44 | else: 45 | print(TEXT["BLACK_HOLE_CRUNCHED"]) 46 | flags.add(game_end) 47 | -------------------------------------------------------------------------------- /solution/06-another-class/space_game.py: -------------------------------------------------------------------------------- 1 | """ 2 | Space Travel Game 3 | 4 | A simple text adventure written for a refactoring tutorial. 5 | """ 6 | 7 | from text_en import TEXT 8 | from puzzles import buy_engine, hire_copilot, stellar_quiz, black_hole 9 | from puzzles import credits, engines, copilot, game_end 10 | 11 | 12 | 13 | class Planet: 14 | 15 | def __init__(self, name, connections, puzzle=None): 16 | self.name = name 17 | self.description = TEXT[name.upper() + "_DESCRIPTION"] 18 | self.connections = connections 19 | self.puzzle = puzzle 20 | 21 | def visit(self, flags): 22 | print(self.description) 23 | if self.puzzle: 24 | self.puzzle(flags) 25 | 26 | 27 | PLANETS = {p.name: p for p in [ 28 | Planet('earth', ["centauri", "sirius"]), 29 | Planet('centauri', ["earth", "orion"], buy_engine), 30 | Planet('sirius', ["orion", "earth", "black_hole"], stellar_quiz), 31 | Planet('orion', ["centauri", "sirius"], hire_copilot), 32 | Planet('black_hole', ["sirius"], black_hole) 33 | ]} 34 | 35 | 36 | class SpaceGame: 37 | 38 | def __init__(self): 39 | self.flags = set() 40 | self.planet = PLANETS["earth"] 41 | 42 | @property 43 | def running(self): 44 | return game_end not in self.flags 45 | 46 | def display_inventory(self): 47 | """Returns a string description of the inventory""" 48 | inventory = "\nYou have: " 49 | inventory += "plenty of credits, " if credits in self.flags else "" 50 | inventory += "a hyperdrive, " if engines in self.flags else "" 51 | inventory += "a skilled copilot, " if copilot in self.flags else "" 52 | if inventory.endswith(", "): 53 | inventory = inventory.strip(", ") 54 | return inventory 55 | 56 | def visit_planet(self): 57 | self.planet.visit(self.flags) 58 | 59 | def display_destinations(self): 60 | """Returns the planet selection menu""" 61 | result = "\nWhere do you want to travel?" 62 | for i, d in enumerate(self.planet.connections, 1): 63 | result += f"[{i}] {d}" 64 | return result 65 | 66 | def select_planet(self): 67 | choice = input() 68 | self.planet = PLANETS[self.planet.connections[int(choice) - 1]] 69 | 70 | 71 | def travel(): 72 | """main game function""" 73 | game = SpaceGame() 74 | 75 | print(TEXT["OPENING_MESSAGE"]) 76 | game.visit_planet() 77 | 78 | while game.running: 79 | print(game.display_destinations()) 80 | game.select_planet() 81 | print('-' * 79) 82 | print(game.display_inventory()) 83 | game.visit_planet() 84 | 85 | 86 | if __name__ == "__main__": 87 | travel() 88 | -------------------------------------------------------------------------------- /solution/06-another-class/test_space_game.py: -------------------------------------------------------------------------------- 1 | import io 2 | import pytest 3 | 4 | from space_game import travel 5 | 6 | 7 | # the actual solution to the game 8 | SOLUTION = [ 9 | "2", 10 | "2", # go to sirius and win quiz 11 | "1", 12 | "42", # hire copilot on orion 13 | "1", 14 | "yes", # go to centauri and buy GPU drive 15 | "2", 16 | "2", 17 | "3", 18 | "yes", # jump into black hole 19 | ] 20 | 21 | DEATH_BY_BLACK_HOLE = [ 22 | "2", 23 | "2", # go to sirius and win quiz 24 | "1", 25 | "41", # hire copilot on orion 26 | "1", 27 | "yes", # go to centauri and buy GPU drive 28 | "1", 29 | "2", 30 | "3", 31 | "yes", # jump into black hole 32 | ] 33 | 34 | # text sniplets that should appear literally in the output 35 | PHRASES = [ 36 | "The stars are waiting for you", 37 | "Betelgeuse", 38 | "credits", 39 | "tech-savvy native", 40 | "copilot", 41 | "buy", 42 | "life, the universe and everything", 43 | "Black Hole", 44 | "stupid idea", 45 | "wonders beyond description", 46 | "THE END", 47 | ] 48 | 49 | 50 | @pytest.fixture 51 | def solution_input(): 52 | """helper function to hijack the keyboard for testing""" 53 | return io.StringIO("\n".join(SOLUTION)) 54 | 55 | 56 | def test_travel(monkeypatch, solution_input): 57 | """game finishes""" 58 | monkeypatch.setattr("sys.stdin", solution_input) 59 | travel() 60 | 61 | 62 | def test_output(monkeypatch, capsys, solution_input): 63 | """text output is not empty""" 64 | monkeypatch.setattr("sys.stdin", solution_input) 65 | 66 | travel() 67 | 68 | captured = capsys.readouterr() 69 | assert len(captured.out) > 0 70 | 71 | 72 | def test_die(monkeypatch, capsys): 73 | """player dies""" 74 | monkeypatch.setattr("sys.stdin", io.StringIO("\n".join(DEATH_BY_BLACK_HOLE))) 75 | 76 | travel() 77 | 78 | captured = capsys.readouterr() 79 | assert "grain of dust" in captured.out 80 | assert " wonders beyond description" not in captured.out 81 | 82 | 83 | @pytest.mark.parametrize("phrase", PHRASES) 84 | def test_output_phrases(monkeypatch, capsys, solution_input, phrase): 85 | """check for some key phrases in the output""" 86 | monkeypatch.setattr("sys.stdin", solution_input) 87 | 88 | travel() 89 | 90 | captured = capsys.readouterr() 91 | assert phrase in captured.out 92 | -------------------------------------------------------------------------------- /solution/06-another-class/text_en.py: -------------------------------------------------------------------------------- 1 | TEXT = { 2 | "OPENING_MESSAGE": """ 3 | ------------------------------------------------------------------------------- 4 | 5 | You and your trusted spaceship set out to look for 6 | fame, wisdom, and adventure. The stars are waiting for you. 7 | """, 8 | "EARTH_DESCRIPTION": "\nYou are on Earth. Beautiful is better than ugly.", 9 | "CENTAURI_DESCRIPTION": "\nYou are on Alpha Centauri. All creatures are welcome here.", 10 | "HYPERDRIVE_SHOPPING_QUESTION": """There is a brand new hyperdrive with a built-in GPU for sale. 11 | 12 | Would you like to buy one [yes/no]""", 13 | "HYPERDRIVE_TOO_EXPENSIVE": """ 14 | You cannot afford it. The GPU is too expensive.""", 15 | "SIRIUS_DESCRIPTION": """ 16 | You are on Sirius. The system is full of media companies and content delivery networks.""", 17 | "SIRIUS_QUIZ_QUESTION": """You manage to get a place in *Stellar* - the greatest quiz show in the universe. 18 | Here is your question: 19 | 20 | Which star do you find on the shoulder of Orion? 21 | 22 | [1] Altair 23 | [2] Betelgeuse 24 | [3] Aldebaran 25 | [4] Andromeda 26 | """, 27 | "SIRIUS_QUIZ_CORRECT": """ 28 | *Correct!!!* You win a ton or credits. 29 | """, 30 | "SIRIUS_QUIZ_INCORRECT": """ 31 | Sorry, this was the wrong answer. Don't take it too sirius. 32 | Better luck next time. 33 | """, 34 | "ORION_DESCRIPTION": """ 35 | You are on Orion. An icy world inhabited by furry sentients.""", 36 | "ORION_HIRE_COPILOT_QUESTION": """A tech-savvy native admires your spaceship. 37 | They promise to join as a copilot if you can answer a question: 38 | 39 | What is the answer to question of life, the universe and everything? 40 | 41 | What do you answer?""", 42 | "COPILOT_QUESTION_CORRECT": """ 43 | Your new copilot jumps on board and immediately starts 44 | configuring new docker containers. 45 | """, 46 | "COPILOT_QUESTION_INCORRECT": """ 47 | Sorry, that's not it. Try again later. 48 | """, 49 | "BLACK_HOLE_DESCRIPTION": """ 50 | You are close to Black Hole #0997. Maybe coming here was a really stupid idea. 51 | Do you want to examine the black hole closer? [yes/no] 52 | """, 53 | "BLACK_HOLE_CRUNCHED": """ 54 | The black hole condenses your spaceship into a grain of dust. 55 | 56 | THE END 57 | """, 58 | "BLACK_HOLE_COPILOT_SAVES_YOU": """ 59 | On the rim of the black hole your copilot blurts out: 60 | 61 | Turn left! 62 | 63 | You ignite the next-gen hyperdrive, creating a time-space anomaly. 64 | You travel through other dimensions and experience wonders beyond description. 65 | """, 66 | "END_CREDITS": """ 67 | THE END 68 | """, 69 | } 70 | -------------------------------------------------------------------------------- /solution/07-oop-decouple-game-logic/puzzles.py: -------------------------------------------------------------------------------- 1 | """ 2 | Puzzle classes that work with decoupled UI / game logic 3 | 4 | implements the Strategy Pattern (puzzle objects combine with any planet) 5 | """ 6 | from text_en import TEXT 7 | from abc import ABC, abstractmethod 8 | 9 | credits, engines, copilot, game_end = range(4) 10 | 11 | 12 | class Puzzle(ABC): 13 | """Abstract Base Class (ABC) for puzzles""" 14 | 15 | @abstractmethod 16 | def is_active(self, flags): 17 | pass 18 | 19 | @abstractmethod 20 | def get_question(self, flags): 21 | pass 22 | 23 | @abstractmethod 24 | def answer(self, flags, answer): 25 | pass 26 | 27 | 28 | class BuyEngine(Puzzle): 29 | 30 | def is_active(self, flags): 31 | return engines not in flags 32 | 33 | def get_question(self, flags): 34 | return TEXT["HYPERDRIVE_SHOPPING_QUESTION"] 35 | 36 | def answer(self, flags, answer): 37 | if answer == "yes": 38 | if credits in flags: 39 | flags.add(engines) 40 | return '' 41 | else: 42 | return TEXT["HYPERDRIVE_TOO_EXPENSIVE"] 43 | return '' 44 | 45 | 46 | class StellarQuiz(Puzzle): 47 | 48 | def is_active(self, flags): 49 | return credits not in flags 50 | 51 | def get_question(self, flags): 52 | return TEXT["SIRIUS_QUIZ_QUESTION"] 53 | 54 | def answer(self, flags, answer): 55 | if answer == "2": 56 | flags.add(credits) 57 | return TEXT["SIRIUS_QUIZ_CORRECT"] 58 | else: 59 | return TEXT["SIRIUS_QUIZ_INCORRECT"] 60 | 61 | 62 | class HireCopilot(Puzzle): 63 | 64 | def is_active(self, flags): 65 | return copilot not in flags 66 | 67 | def get_question(self, flags): 68 | return TEXT["ORION_HIRE_COPILOT_QUESTION"] 69 | 70 | def answer(self, flags, answer): 71 | if answer == "42": 72 | flags.add(copilot) 73 | return TEXT["COPILOT_QUESTION_CORRECT"] 74 | else: 75 | return TEXT["COPILOT_QUESTION_INCORRECT"] 76 | 77 | 78 | class BlackHole(Puzzle): 79 | 80 | def is_active(self, flags): 81 | return True 82 | 83 | def get_question(self, flags): 84 | return '' 85 | 86 | def answer(self, flags, answer): 87 | if answer == "yes": 88 | flags.add(game_end) 89 | if engines in flags and copilot in flags: 90 | return TEXT["BLACK_HOLE_COPILOT_SAVES_YOU"] + TEXT["END_CREDITS"] 91 | else: 92 | return TEXT["BLACK_HOLE_CRUNCHED"] 93 | return '' 94 | -------------------------------------------------------------------------------- /solution/07-oop-decouple-game-logic/space_game.py: -------------------------------------------------------------------------------- 1 | """ 2 | Space Travel Game 3 | 4 | A simple text adventure written for a refactoring tutorial. 5 | """ 6 | 7 | from text_en import TEXT 8 | from puzzles import StellarQuiz, BuyEngine, HireCopilot, BlackHole 9 | from puzzles import credits, engines, copilot, game_end 10 | 11 | 12 | class Planet: 13 | 14 | def __init__(self, name, connections, puzzle=None): 15 | self.name = name 16 | self.description = TEXT[name.upper() + "_DESCRIPTION"] 17 | self.connections = connections 18 | self.puzzle = puzzle 19 | 20 | def has_active_puzzle(self, flags): 21 | return self.puzzle and self.puzzle.is_active(flags) 22 | 23 | def get_puzzle_text(self, flags): 24 | return self.puzzle.get_question(flags) 25 | 26 | def answer_puzzle(self, flags, action): 27 | return self.puzzle.answer(flags, action) 28 | 29 | 30 | PLANETS = {p.name: p for p in [ 31 | Planet('earth', ["centauri", "sirius"]), 32 | Planet('centauri', ["earth", "orion"], BuyEngine()), 33 | Planet('sirius', ["orion", "earth", "black_hole"], StellarQuiz()), 34 | Planet('orion', ["centauri", "sirius"], HireCopilot()), 35 | Planet('black_hole', ["sirius"], BlackHole()) 36 | ]} 37 | 38 | 39 | class SpaceGame: 40 | 41 | def __init__(self): 42 | self.flags = set() 43 | self.planet = PLANETS["earth"] 44 | self.state = 'start' 45 | 46 | @property 47 | def running(self): 48 | return game_end not in self.flags 49 | 50 | def display_inventory(self): 51 | """Returns a string description of the inventory""" 52 | inventory = "\nYou have: " 53 | inventory += "plenty of credits, " if credits in self.flags else "" 54 | inventory += "a hyperdrive, " if engines in self.flags else "" 55 | inventory += "a skilled copilot, " if copilot in self.flags else "" 56 | if inventory.endswith(", "): 57 | return inventory.strip(", ") 58 | return '' 59 | 60 | def visit_planet(self): 61 | self.planet.visit(self.flags) 62 | 63 | @property 64 | def choices(self): 65 | if self.state == 'move': 66 | return self.planet.connections 67 | elif self.state == 'puzzle': 68 | return None 69 | 70 | def get_situation_text(self): 71 | result = '' 72 | if self.state == 'start': 73 | result = TEXT["OPENING_MESSAGE"] 74 | self.state = 'move' 75 | if self.state == 'move': 76 | result += self.display_inventory() 77 | result += "\n\nWhere do you want to travel?" 78 | if self.state == 'puzzle': 79 | result = self.planet.get_puzzle_text(self.flags) 80 | return result 81 | 82 | def take_action(self, action): 83 | """manages state transitions""" 84 | if self.state == 'move': 85 | self.planet = PLANETS[action] 86 | if self.planet.has_active_puzzle(self.flags): 87 | self.state = 'puzzle' 88 | return self.planet.description 89 | 90 | if self.state == 'puzzle': 91 | self.state = 'move' 92 | return(self.planet.answer_puzzle(self.flags, action)) 93 | 94 | 95 | # 96 | # User Interface 97 | # 98 | # the only part of the program that knows about print() and input() 99 | # 100 | def display_options(choices): 101 | """Returns a generic selection menu""" 102 | if choices: 103 | for i, d in enumerate(choices, 1): 104 | print(f"[{i}] {d}") 105 | 106 | def select_option(choices): 107 | """Returns keyboard input""" 108 | action = input() 109 | if choices: 110 | return choices[int(action) - 1] 111 | return action 112 | 113 | 114 | def travel(): 115 | """main game function""" 116 | game = SpaceGame() 117 | while game.running: 118 | print('-' * 79) 119 | print(game.get_situation_text()) 120 | display_options(game.choices) 121 | action = select_option(game.choices) 122 | print(game.take_action(action)) 123 | 124 | 125 | if __name__ == "__main__": 126 | travel() 127 | -------------------------------------------------------------------------------- /solution/07-oop-decouple-game-logic/test_space_game.py: -------------------------------------------------------------------------------- 1 | import io 2 | import pytest 3 | 4 | from space_game import travel 5 | 6 | 7 | # the actual solution to the game 8 | SOLUTION = [ 9 | "2", 10 | "2", # go to sirius and win quiz 11 | "1", 12 | "42", # hire copilot on orion 13 | "1", 14 | "yes", # go to centauri and buy GPU drive 15 | "2", 16 | "2", 17 | "3", 18 | "yes", # jump into black hole 19 | ] 20 | 21 | DEATH_BY_BLACK_HOLE = [ 22 | "2", 23 | "2", # go to sirius and win quiz 24 | "1", 25 | "41", # hire copilot on orion 26 | "1", 27 | "yes", # go to centauri and buy GPU drive 28 | "1", 29 | "2", 30 | "3", 31 | "yes", # jump into black hole 32 | ] 33 | 34 | # text sniplets that should appear literally in the output 35 | PHRASES = [ 36 | "The stars are waiting for you", 37 | "Betelgeuse", 38 | "credits", 39 | "tech-savvy native", 40 | "copilot", 41 | "buy", 42 | "life, the universe and everything", 43 | "Black Hole", 44 | "stupid idea", 45 | "wonders beyond description", 46 | "THE END", 47 | ] 48 | 49 | 50 | @pytest.fixture 51 | def solution_input(): 52 | """helper function to hijack the keyboard for testing""" 53 | return io.StringIO("\n".join(SOLUTION)) 54 | 55 | 56 | def test_travel(monkeypatch, solution_input): 57 | """game finishes""" 58 | monkeypatch.setattr("sys.stdin", solution_input) 59 | travel() 60 | 61 | 62 | def test_output(monkeypatch, capsys, solution_input): 63 | """text output is not empty""" 64 | monkeypatch.setattr("sys.stdin", solution_input) 65 | 66 | travel() 67 | 68 | captured = capsys.readouterr() 69 | assert len(captured.out) > 0 70 | 71 | 72 | def test_die(monkeypatch, capsys): 73 | """player dies""" 74 | monkeypatch.setattr("sys.stdin", io.StringIO("\n".join(DEATH_BY_BLACK_HOLE))) 75 | 76 | travel() 77 | 78 | captured = capsys.readouterr() 79 | assert "grain of dust" in captured.out 80 | assert " wonders beyond description" not in captured.out 81 | 82 | 83 | @pytest.mark.parametrize("phrase", PHRASES) 84 | def test_output_phrases(monkeypatch, capsys, solution_input, phrase): 85 | """check for some key phrases in the output""" 86 | monkeypatch.setattr("sys.stdin", solution_input) 87 | 88 | travel() 89 | 90 | captured = capsys.readouterr() 91 | assert phrase in captured.out 92 | -------------------------------------------------------------------------------- /solution/07-oop-decouple-game-logic/text_en.py: -------------------------------------------------------------------------------- 1 | TEXT = { 2 | "OPENING_MESSAGE": """ 3 | You and your trusted spaceship set out to look for 4 | fame, wisdom, and adventure. The stars are waiting for you. 5 | """, 6 | "EARTH_DESCRIPTION": "\nYou are on Earth. Beautiful is better than ugly.", 7 | "CENTAURI_DESCRIPTION": "\nYou are on Alpha Centauri. All creatures are welcome here.", 8 | "HYPERDRIVE_SHOPPING_QUESTION": """There is a brand new hyperdrive with a built-in GPU for sale. 9 | 10 | Would you like to buy one [yes/no]""", 11 | "HYPERDRIVE_TOO_EXPENSIVE": """ 12 | You cannot afford it. The GPU is too expensive.""", 13 | "SIRIUS_DESCRIPTION": """ 14 | You are on Sirius. The system is full of media companies and content delivery networks.""", 15 | "SIRIUS_QUIZ_QUESTION": """You manage to get a place in *Stellar* - the greatest quiz show in the universe. 16 | Here is your question: 17 | 18 | Which star do you find on the shoulder of Orion? 19 | 20 | [1] Altair 21 | [2] Betelgeuse 22 | [3] Aldebaran 23 | [4] Andromeda 24 | """, 25 | "SIRIUS_QUIZ_CORRECT": """ 26 | *Correct!!!* You win a ton or credits. 27 | """, 28 | "SIRIUS_QUIZ_INCORRECT": """ 29 | Sorry, this was the wrong answer. Don't take it too sirius. 30 | Better luck next time. 31 | """, 32 | "ORION_DESCRIPTION": """ 33 | You are on Orion. An icy world inhabited by furry sentients.""", 34 | "ORION_HIRE_COPILOT_QUESTION": """A tech-savvy native admires your spaceship. 35 | They promise to join as a copilot if you can answer a question: 36 | 37 | What is the answer to question of life, the universe and everything? 38 | 39 | What do you answer?""", 40 | "COPILOT_QUESTION_CORRECT": """ 41 | Your new copilot jumps on board and immediately starts 42 | configuring new docker containers. 43 | """, 44 | "COPILOT_QUESTION_INCORRECT": """ 45 | Sorry, that's not it. Try again later. 46 | """, 47 | "BLACK_HOLE_DESCRIPTION": """ 48 | You are close to Black Hole #0997. Maybe coming here was a really stupid idea. 49 | Do you want to examine the black hole closer? [yes/no] 50 | """, 51 | "BLACK_HOLE_CRUNCHED": """ 52 | The black hole condenses your spaceship into a grain of dust. 53 | 54 | THE END 55 | """, 56 | "BLACK_HOLE_COPILOT_SAVES_YOU": """ 57 | On the rim of the black hole your copilot blurts out: 58 | 59 | Turn left! 60 | 61 | You ignite the next-gen hyperdrive, creating a time-space anomaly. 62 | You travel through other dimensions and experience wonders beyond description. 63 | """, 64 | "END_CREDITS": """ 65 | THE END 66 | """, 67 | } 68 | -------------------------------------------------------------------------------- /solution_jan24/space_game.py: -------------------------------------------------------------------------------- 1 | """ 2 | Space Travel Game 3 | 4 | A simple text adventure written for a refactoring tutorial. 5 | """ 6 | from text_en import TEXT 7 | 8 | 9 | def display_inventory(credits: bool, engines: bool, copilot: bool) -> None: 10 | print("-" * 79) 11 | inventory = "\nYou have: " 12 | inventory += "plenty of credits, " if credits else "" 13 | inventory += "a hyperdrive, " if engines else "" 14 | inventory += "a skilled copilot, " if copilot else "" 15 | if inventory.endswith(", "): 16 | print(inventory.strip(", ")) 17 | 18 | 19 | def select_planet(destinations: list[str]) -> str: 20 | print("\nWhere do you want to travel?") 21 | position = 1 22 | for d in destinations: 23 | print(f"[{position}] {d}") 24 | position += 1 25 | choice = input() 26 | return destinations[int(choice) - 1] 27 | 28 | 29 | def visit_planet(planet: str, credits: bool, engines: bool, copilot: bool, game_end: bool): 30 | if planet == "earth": 31 | destinations = ["centauri", "sirius"] 32 | print(TEXT["EARTH_DESCRIPTION"]) 33 | 34 | if planet == "centauri": 35 | print(TEXT["CENTAURI_DESCRIPTION"]) 36 | destinations = ["earth", "orion"] 37 | 38 | if not engines: 39 | print(TEXT["HYPERDRIVE_SHOPPING_QUESTION"]) 40 | if input() == "yes": 41 | if credits: 42 | engines = True 43 | else: 44 | print(TEXT["HYPERDRIVE_TOO_EXPENSIVE"]) 45 | 46 | if planet == "sirius": 47 | print(TEXT["SIRIUS_DESCRIPTION"]) 48 | destinations = ["orion", "earth", "black_hole"] 49 | 50 | if not credits: 51 | print(TEXT["SIRIUS_QUIZ_QUESTION"]) 52 | answer = input() 53 | if answer == "2": 54 | print(TEXT["SIRIUS_QUIZ_CORRECT"]) 55 | credits = True 56 | else: 57 | print(TEXT["SIRIUS_QUIZ_INCORRECT"]) 58 | 59 | if planet == "orion": 60 | destinations = ["centauri", "sirius"] 61 | if not copilot: 62 | print(TEXT["ORION_DESCRIPTION"]) 63 | print(TEXT["ORION_HIRE_COPILOT_QUESTION"]) 64 | if input() == "42": 65 | print(TEXT["COPILOT_QUESTION_CORRECT"]) 66 | copilot = True 67 | else: 68 | print(TEXT["COPILOT_QUESTION_INCORRECT"]) 69 | else: 70 | print(TEXT["ORION_DESCRIPTION"]) 71 | 72 | if planet == "black_hole": 73 | print(TEXT["BLACK_HOLE_DESCRIPTION"]) 74 | destinations = ["sirius"] 75 | if input() == "yes": 76 | if engines and copilot: 77 | print(TEXT["BLACK_HOLE_COPILOT_SAVES_YOU"]) 78 | game_end = True 79 | else: 80 | print(TEXT["BLACK_HOLE_CRUNCHED"]) 81 | return 82 | 83 | return destinations, credits, engines, copilot, game_end 84 | 85 | 86 | def travel(): 87 | print(TEXT["OPENING_MESSAGE"]) 88 | 89 | planet = "earth" 90 | engines = False 91 | copilot = False 92 | credits = False 93 | game_end = False 94 | 95 | while not game_end: 96 | display_inventory(credits=credits, engines=engines, copilot=copilot) 97 | destinations, credits, engines, copilot, game_end = visit_planet( 98 | planet=planet, 99 | credits=credits, 100 | engines=engines, 101 | copilot=copilot, 102 | game_end=game_end 103 | ) 104 | 105 | if not game_end: 106 | planet = select_planet(destinations=destinations) 107 | 108 | print(TEXT["END_CREDITS"]) 109 | 110 | 111 | if __name__ == "__main__": 112 | travel() 113 | -------------------------------------------------------------------------------- /solution_jan24/space_game_chatgpt.py: -------------------------------------------------------------------------------- 1 | 2 | from text_en import TEXT 3 | 4 | 5 | class SpaceTravelGame: 6 | def __init__(self): 7 | self.planet = "earth" 8 | self.engines = False 9 | self.copilot = False 10 | self.credits = False 11 | self.game_end = False 12 | 13 | def display_inventory(self): 14 | print("-" * 79) 15 | inventory = "\nYou have: " 16 | inventory += "plenty of credits, " if self.credits else "" 17 | inventory += "a hyperdrive, " if self.engines else "" 18 | inventory += "a skilled copilot, " if self.copilot else "" 19 | if inventory.endswith(", "): 20 | print(inventory.strip(", ")) 21 | 22 | def interact_with_earth(self): 23 | destinations = ["centauri", "sirius"] 24 | print(TEXT["EARTH_DESCRIPTION"]) 25 | return destinations 26 | 27 | def interact_with_centauri(self): 28 | destinations = ["earth", "orion"] 29 | print(TEXT["CENTAURI_DESCRIPTION"]) 30 | if not self.engines: 31 | print(TEXT["HYPERDRIVE_SHOPPING_QUESTION"]) 32 | if input() == "yes" and self.credits: 33 | self.engines = True 34 | else: 35 | print(TEXT["HYPERDRIVE_TOO_EXPENSIVE"]) 36 | return destinations 37 | 38 | def interact_with_sirius(self): 39 | destinations = ["orion", "earth", "black_hole"] 40 | print(TEXT["SIRIUS_DESCRIPTION"]) 41 | if not self.credits: 42 | print(TEXT["SIRIUS_QUIZ_QUESTION"]) 43 | answer = input() 44 | if answer == "2": 45 | print(TEXT["SIRIUS_QUIZ_CORRECT"]) 46 | self.credits = True 47 | else: 48 | print(TEXT["SIRIUS_QUIZ_INCORRECT"]) 49 | return destinations 50 | 51 | def interact_with_orion(self): 52 | destinations = ["centauri", "sirius"] 53 | print(TEXT["ORION_DESCRIPTION"]) 54 | if not self.copilot: 55 | print(TEXT["ORION_HIRE_COPILOT_QUESTION"]) 56 | if input() == "42": 57 | print(TEXT["COPILOT_QUESTION_CORRECT"]) 58 | self.copilot = True 59 | return destinations 60 | 61 | def interact_with_black_hole(self): 62 | destinations = ["sirius"] 63 | print(TEXT["BLACK_HOLE_DESCRIPTION"]) 64 | if input() == "yes": 65 | if self.engines and self.copilot: 66 | print(TEXT["BLACK_HOLE_COPILOT_SAVES_YOU"]) 67 | self.game_end = True 68 | else: 69 | print(TEXT["BLACK_HOLE_CRUNCHED"]) 70 | return destinations 71 | 72 | def select_next_planet(self, destinations): 73 | print("\nWhere do you want to travel?") 74 | position = 1 75 | for d in destinations: 76 | print(f"[{position}] {d}") 77 | position += 1 78 | 79 | choice = input() 80 | self.planet = destinations[int(choice) - 1] 81 | 82 | def play_game(self): 83 | print(TEXT["OPENING_MESSAGE"]) 84 | 85 | while not self.game_end: 86 | self.display_inventory() 87 | 88 | if self.planet == "earth": 89 | destinations = self.interact_with_earth() 90 | 91 | elif self.planet == "centauri": 92 | destinations = self.interact_with_centauri() 93 | 94 | elif self.planet == "sirius": 95 | destinations = self.interact_with_sirius() 96 | 97 | elif self.planet == "orion": 98 | destinations = self.interact_with_orion() 99 | 100 | elif self.planet == "black_hole": 101 | destinations = self.interact_with_black_hole() 102 | 103 | self.select_next_planet(destinations) 104 | 105 | print(TEXT["END_CREDITS"]) 106 | 107 | 108 | if __name__ == "__main__": 109 | game = SpaceTravelGame() 110 | game.play_game() 111 | -------------------------------------------------------------------------------- /solution_jan24/space_game_final.py: -------------------------------------------------------------------------------- 1 | """ 2 | Space Travel Game 3 | 4 | A simple text adventure written for a refactoring tutorial. 5 | """ 6 | from text_en import TEXT 7 | from typing import Literal, Callable 8 | from pydantic import BaseModel 9 | 10 | 11 | Flag = Literal["engines", "credits", "copilot", "game_end"] 12 | 13 | # oldschool 14 | # ENGINES, CREDITS, COPILOT, GAME_END = range(4) 15 | 16 | 17 | def display_inventory(flags: set[Flag]) -> None: 18 | print("-" * 79) 19 | inventory = "\nYou have: " 20 | inventory += "plenty of credits, " if "credits" in flags else "" 21 | inventory += "a hyperdrive, " if "engines" in flags else "" 22 | inventory += "a skilled copilot, " if "copilot" in flags else "" 23 | if inventory.endswith(", "): 24 | print(inventory.strip(", ")) 25 | 26 | 27 | def select_planet(destinations: list[str]) -> str: 28 | print("\nWhere do you want to travel?") 29 | position = 1 30 | for d in destinations: 31 | print(f"[{position}] {d}") 32 | position += 1 33 | choice = input() 34 | return destinations[int(choice) - 1] 35 | 36 | 37 | 38 | def buy_hyperdrive(flags): 39 | if not "engines" in flags: 40 | print(TEXT["HYPERDRIVE_SHOPPING_QUESTION"]) 41 | if input() == "yes": 42 | if "credits" in flags: 43 | flags.add("engines") 44 | else: 45 | print(TEXT["HYPERDRIVE_TOO_EXPENSIVE"]) 46 | 47 | 48 | def quiz_show(flags): 49 | if not "credits" in flags: 50 | print(TEXT["SIRIUS_QUIZ_QUESTION"]) 51 | answer = input() 52 | if answer == "2": 53 | print(TEXT["SIRIUS_QUIZ_CORRECT"]) 54 | flags.add("credits") 55 | else: 56 | print(TEXT["SIRIUS_QUIZ_INCORRECT"]) 57 | 58 | 59 | def hire_copilot(flags): 60 | if not "copilot" in flags: 61 | print(TEXT["ORION_HIRE_COPILOT_QUESTION"]) 62 | if input() == "42": 63 | print(TEXT["COPILOT_QUESTION_CORRECT"]) 64 | flags.add("copilot") 65 | else: 66 | print(TEXT["COPILOT_QUESTION_INCORRECT"]) 67 | 68 | 69 | def navigate_black_hole(flags): 70 | if input() == "yes": 71 | flags.add("game_end") 72 | if "engines" in flags and "copilot" in flags: 73 | print(TEXT["BLACK_HOLE_COPILOT_SAVES_YOU"]) 74 | else: 75 | print(TEXT["BLACK_HOLE_CRUNCHED"]) 76 | 77 | 78 | class Planet(BaseModel): 79 | name: str 80 | description: str 81 | connections: list[str] 82 | puzzle_function: Callable = None 83 | 84 | def visit(self, flags: set[Flag]): 85 | print(self.description) 86 | if self.puzzle_function: 87 | self.puzzle_function(flags) 88 | 89 | 90 | Planets = { 91 | "earth": Planet( 92 | name = "earth", 93 | description = TEXT["EARTH_DESCRIPTION"], 94 | connections = ["centauri", "sirius"], 95 | ), 96 | "centauri": Planet( 97 | name = "centauri", 98 | description = TEXT["CENTAURI_DESCRIPTION"], 99 | connections = ["earth", "orion"], 100 | puzzle_function = buy_hyperdrive, 101 | ), 102 | "sirius": Planet( 103 | name = "sirius", 104 | description = TEXT["SIRIUS_DESCRIPTION"], 105 | connections = ["orion", "earth", "black_hole"], 106 | puzzle_function = quiz_show, 107 | ), 108 | "orion": Planet( 109 | name = "orion", 110 | description = TEXT["ORION_DESCRIPTION"], 111 | connections = ["centauri", "sirius"], 112 | puzzle_function = hire_copilot, 113 | ), 114 | "black_hole": Planet( 115 | name = "black_hole", 116 | description = TEXT["BLACK_HOLE_DESCRIPTION"], 117 | connections=["sirius"], 118 | puzzle_function = navigate_black_hole 119 | ) 120 | } 121 | 122 | 123 | def travel() -> None: 124 | print(TEXT["OPENING_MESSAGE"]) 125 | 126 | planet = Planets["earth"] 127 | flags: set[Flag] = set() 128 | 129 | while not "game_end" in flags: 130 | display_inventory(flags=flags) 131 | planet.visit(flags=flags) 132 | if not "game_end" in flags: 133 | planet = Planets[select_planet(destinations=planet.connections)] 134 | 135 | print(TEXT["END_CREDITS"]) 136 | 137 | 138 | if __name__ == "__main__": 139 | travel() 140 | -------------------------------------------------------------------------------- /solution_jan24/test_space_game.py: -------------------------------------------------------------------------------- 1 | import io 2 | import pytest 3 | 4 | from space_game import travel 5 | 6 | 7 | # the actual solution to the game 8 | SOLUTION = [ 9 | "2", 10 | "2", # go to sirius and win quiz 11 | "1", 12 | "42", # hire copilot on orion 13 | "1", 14 | "yes", # go to centauri and buy GPU drive 15 | "2", 16 | "2", 17 | "3", 18 | "yes", # jump into black hole 19 | ] 20 | 21 | DEATH_BY_BLACK_HOLE = [ 22 | "2", 23 | "2", # go to sirius and win quiz 24 | "1", 25 | "41", # hire copilot on orion 26 | "1", 27 | "yes", # go to centauri and buy GPU drive 28 | "1", 29 | "2", 30 | "3", 31 | "yes", # jump into black hole 32 | ] 33 | 34 | # text sniplets that should appear literally in the output 35 | PHRASES = [ 36 | "The stars are waiting for you", 37 | "Betelgeuse", 38 | "credits", 39 | "tech-savvy native", 40 | "copilot", 41 | "buy", 42 | "life, the universe and everything", 43 | "Black Hole", 44 | "stupid idea", 45 | "wonders beyond description", 46 | "THE END", 47 | ] 48 | 49 | 50 | @pytest.fixture 51 | def solution_input(): 52 | """helper function to hijack the keyboard for testing""" 53 | return io.StringIO("\n".join(SOLUTION)) 54 | 55 | 56 | def test_travel(monkeypatch, solution_input): 57 | """game finishes""" 58 | monkeypatch.setattr("sys.stdin", solution_input) 59 | travel() 60 | 61 | 62 | def test_output(monkeypatch, capsys, solution_input): 63 | """text output is not empty""" 64 | monkeypatch.setattr("sys.stdin", solution_input) 65 | 66 | travel() 67 | 68 | captured = capsys.readouterr() 69 | assert len(captured.out) > 0 70 | 71 | 72 | def test_die(monkeypatch, capsys): 73 | """player dies""" 74 | monkeypatch.setattr("sys.stdin", io.StringIO("\n".join(DEATH_BY_BLACK_HOLE))) 75 | 76 | travel() 77 | 78 | captured = capsys.readouterr() 79 | assert "grain of dust" in captured.out 80 | assert " wonders beyond description" not in captured.out 81 | 82 | 83 | @pytest.mark.parametrize("phrase", PHRASES) 84 | def test_output_phrases(monkeypatch, capsys, solution_input, phrase): 85 | """check for some key phrases in the output""" 86 | monkeypatch.setattr("sys.stdin", solution_input) 87 | 88 | travel() 89 | 90 | captured = capsys.readouterr() 91 | assert phrase in captured.out 92 | -------------------------------------------------------------------------------- /solution_jan24/text_en.py: -------------------------------------------------------------------------------- 1 | 2 | TEXT: dict[str, str] = { 3 | "OPENING_MESSAGE": """ 4 | ------------------------------------------------------------------------------- 5 | 6 | You and your trusted spaceship set out to look for 7 | fame, wisdom, and adventure. The stars are waiting for you. 8 | """, 9 | "EARTH_DESCRIPTION": "\nYou are on Earth. Beautiful is better than ugly.", 10 | "CENTAURI_DESCRIPTION": "\nYou are on Alpha Centauri. All creatures are welcome here.", 11 | "HYPERDRIVE_SHOPPING_QUESTION": """There is a brand new hyperdrive with a built-in GPU for sale. 12 | 13 | Would you like to buy one [yes/no]""", 14 | "HYPERDRIVE_TOO_EXPENSIVE": """ 15 | You cannot afford it. The GPU is too expensive.""", 16 | "SIRIUS_DESCRIPTION": """ 17 | You are on Sirius. The system is full of media companies and content delivery networks.""", 18 | "SIRIUS_QUIZ_QUESTION": """You manage to get a place in *Stellar* - the greatest quiz show in the universe. 19 | Here is your question: 20 | 21 | Which star do you find on the shoulder of Orion? 22 | 23 | [1] Altair 24 | [2] Betelgeuse 25 | [3] Aldebaran 26 | [4] Andromeda 27 | """, 28 | "SIRIUS_QUIZ_CORRECT": """ 29 | *Correct!!!* You win a ton or credits. 30 | """, 31 | "SIRIUS_QUIZ_INCORRECT": """ 32 | Sorry, this was the wrong answer. Don't take it too sirius. 33 | Better luck next time. 34 | """, 35 | "ORION_DESCRIPTION": """ 36 | You are on Orion. An icy world inhabited by furry sentients.""", 37 | "ORION_HIRE_COPILOT_QUESTION": """A tech-savvy native admires your spaceship. 38 | They promise to join as a copilot if you can answer a question: 39 | 40 | What is the answer to question of life, the universe and everything? 41 | 42 | What do you answer?""", 43 | "COPILOT_QUESTION_CORRECT": """ 44 | Your new copilot jumps on board and immediately starts 45 | configuring new docker containers. 46 | """, 47 | "COPILOT_QUESTION_INCORRECT": """ 48 | Sorry, that's not it. Try again later. 49 | """, 50 | "BLACK_HOLE_DESCRIPTION": """ 51 | You are close to Black Hole #0997. Maybe coming here was a really stupid idea. 52 | Do you want to examine the black hole closer? [yes/no] 53 | """, 54 | "BLACK_HOLE_CRUNCHED": """ 55 | The black hole condenses your spaceship into a grain of dust. 56 | 57 | THE END 58 | """, 59 | "BLACK_HOLE_COPILOT_SAVES_YOU": """ 60 | On the rim of the black hole your copilot blurts out: 61 | 62 | Turn left! 63 | 64 | You ignite the next-gen hyperdrive, creating a time-space anomaly. 65 | You travel through other dimensions and experience wonders beyond description. 66 | """, 67 | "END_CREDITS": """ 68 | THE END 69 | """, 70 | } 71 | -------------------------------------------------------------------------------- /solution_pycon22/space_game.py: -------------------------------------------------------------------------------- 1 | """ 2 | Space Travel Game 3 | 4 | A simple text adventure written for a refactoring tutorial. 5 | 6 | github.com/krother/refactoring_tutorial 7 | 8 | pytest test_space_game.py 9 | 10 | 1. select a code section 11 | 2. name the function 12 | 3. indent the code 13 | 4. make a parameter out of every variable 14 | not created inside the function 15 | 5. Add a return statement 16 | 6. Add a function call 17 | 7. Run the tests 18 | """ 19 | from text_en import TEXT 20 | 21 | #TODO: use an Enum 22 | credits, engines, copilot, game_end = range(4) 23 | 24 | 25 | def display_inventory(flags): 26 | print("-" * 79) 27 | inventory = "\nYou have: " 28 | inventory += "plenty of credits, " if credits in flags else "" 29 | inventory += "a hyperdrive, " if engines in flags else "" 30 | inventory += "a skilled copilot, " if copilot in flags else "" 31 | if inventory.endswith(", "): 32 | print(inventory.strip(", ")) 33 | 34 | 35 | def select_planet(destinations): 36 | print("\nWhere do you want to travel?") 37 | for i, d in enumerate(destinations, 1): 38 | print(f"[{i}] {d}") 39 | 40 | choice = input() 41 | return destinations[int(choice) - 1] 42 | 43 | 44 | def engine_puzzle(flags): 45 | if engines not in flags: 46 | print(TEXT["HYPERDRIVE_SHOPPING_QUESTION"]) 47 | if input() == "yes": 48 | if credits in flags: 49 | flags.add(engines) 50 | else: 51 | print(TEXT["HYPERDRIVE_TOO_EXPENSIVE"]) 52 | 53 | def quiz(flags): 54 | if credits not in flags: 55 | print(TEXT["SIRIUS_QUIZ_QUESTION"]) 56 | answer = input() 57 | if answer == "2": 58 | print(TEXT["SIRIUS_QUIZ_CORRECT"]) 59 | flags.add(credits) 60 | else: 61 | print(TEXT["SIRIUS_QUIZ_INCORRECT"]) 62 | 63 | def jump_into(flags): 64 | if input() == "yes": 65 | if engines in flags and copilot in flags: 66 | print(TEXT["BLACK_HOLE_COPILOT_SAVES_YOU"]) 67 | else: 68 | print(TEXT["BLACK_HOLE_CRUNCHED"]) 69 | flags.add(game_end) 70 | 71 | 72 | def hire_copilot(flags): 73 | if copilot not in flags: 74 | print(TEXT["ORION_HIRE_COPILOT_QUESTION"]) 75 | if input() == "42": 76 | print(TEXT["COPILOT_QUESTION_CORRECT"]) 77 | flags.add(copilot) 78 | else: 79 | print(TEXT["COPILOT_QUESTION_INCORRECT"]) 80 | 81 | 82 | def do_nothing(flags): 83 | pass 84 | 85 | 86 | class Planet: 87 | 88 | def __init__(self, name, destinations, puzzle=do_nothing): 89 | self.name = name 90 | self.description = TEXT[name.upper() + '_DESCRIPTION'] 91 | self.destinations = destinations 92 | self.puzzle = puzzle 93 | 94 | def visit(self, flags): 95 | """interaction with planets""" 96 | print(self.description) 97 | self.puzzle(flags) 98 | 99 | PLANETS = { 100 | p.name: p for p in [ 101 | Planet('earth', ["centauri", "sirius"]), 102 | Planet('sirius', ["orion", "earth", "black_hole"], quiz), 103 | Planet('orion', ["centauri", "sirius"], hire_copilot), 104 | Planet('centauri', ["earth", "orion"], engine_puzzle), 105 | Planet('black_hole', ["sirius"], jump_into) 106 | ] 107 | } 108 | 109 | def travel(): 110 | print(TEXT["OPENING_MESSAGE"]) 111 | 112 | planet = PLANETS["earth"] 113 | flags = set() 114 | 115 | while game_end not in flags: 116 | display_inventory(flags) 117 | planet.visit(flags) 118 | if game_end not in flags: 119 | planet = PLANETS[select_planet(planet.destinations)] 120 | 121 | print(TEXT["END_CREDITS"]) 122 | 123 | 124 | if __name__ == "__main__": 125 | travel() 126 | -------------------------------------------------------------------------------- /solution_pycon22/test_space_game.py: -------------------------------------------------------------------------------- 1 | import io 2 | import pytest 3 | 4 | from space_game import travel 5 | 6 | 7 | # the actual solution to the game 8 | SOLUTION = [ 9 | "2", 10 | "2", # go to sirius and win quiz 11 | "1", 12 | "42", # hire copilot on orion 13 | "1", 14 | "yes", # go to centauri and buy GPU drive 15 | "2", 16 | "2", 17 | "3", 18 | "yes", # jump into black hole 19 | ] 20 | 21 | DEATH_BY_BLACK_HOLE = [ 22 | "2", 23 | "2", # go to sirius and win quiz 24 | "1", 25 | "41", # hire copilot on orion 26 | "1", 27 | "yes", # go to centauri and buy GPU drive 28 | "1", 29 | "2", 30 | "3", 31 | "yes", # jump into black hole 32 | ] 33 | 34 | # text sniplets that should appear literally in the output 35 | PHRASES = [ 36 | "The stars are waiting for you", 37 | "Betelgeuse", 38 | "credits", 39 | "tech-savvy native", 40 | "copilot", 41 | "buy", 42 | "life, the universe and everything", 43 | "Black Hole", 44 | "stupid idea", 45 | "wonders beyond description", 46 | "THE END", 47 | ] 48 | 49 | 50 | @pytest.fixture 51 | def solution_input(): 52 | """helper function to hijack the keyboard for testing""" 53 | return io.StringIO("\n".join(SOLUTION)) 54 | 55 | 56 | def test_travel(monkeypatch, solution_input): 57 | """game finishes""" 58 | monkeypatch.setattr("sys.stdin", solution_input) 59 | travel() 60 | 61 | 62 | def test_output(monkeypatch, capsys, solution_input): 63 | """text output is not empty""" 64 | monkeypatch.setattr("sys.stdin", solution_input) 65 | 66 | travel() 67 | 68 | captured = capsys.readouterr() 69 | assert len(captured.out) > 0 70 | 71 | 72 | def test_die(monkeypatch, capsys): 73 | """player dies""" 74 | monkeypatch.setattr("sys.stdin", io.StringIO("\n".join(DEATH_BY_BLACK_HOLE))) 75 | 76 | travel() 77 | 78 | captured = capsys.readouterr() 79 | assert "grain of dust" in captured.out 80 | assert " wonders beyond description" not in captured.out 81 | 82 | 83 | @pytest.mark.parametrize("phrase", PHRASES) 84 | def test_output_phrases(monkeypatch, capsys, solution_input, phrase): 85 | """check for some key phrases in the output""" 86 | monkeypatch.setattr("sys.stdin", solution_input) 87 | 88 | travel() 89 | 90 | captured = capsys.readouterr() 91 | assert phrase in captured.out 92 | -------------------------------------------------------------------------------- /solution_pycon22/text_en.py: -------------------------------------------------------------------------------- 1 | TEXT = { 2 | "OPENING_MESSAGE": """ 3 | ------------------------------------------------------------------------------- 4 | 5 | You and your trusted spaceship set out to look for 6 | fame, wisdom, and adventure. The stars are waiting for you. 7 | """, 8 | "EARTH_DESCRIPTION": "\nYou are on Earth. Beautiful is better than ugly.", 9 | "CENTAURI_DESCRIPTION": "\nYou are on Alpha Centauri. All creatures are welcome here.", 10 | "HYPERDRIVE_SHOPPING_QUESTION": """There is a brand new hyperdrive with a built-in GPU for sale. 11 | 12 | Would you like to buy one [yes/no]""", 13 | "HYPERDRIVE_TOO_EXPENSIVE": """ 14 | You cannot afford it. The GPU is too expensive.""", 15 | "SIRIUS_DESCRIPTION": """ 16 | You are on Sirius. The system is full of media companies and content delivery networks.""", 17 | "SIRIUS_QUIZ_QUESTION": """You manage to get a place in *Stellar* - the greatest quiz show in the universe. 18 | Here is your question: 19 | 20 | Which star do you find on the shoulder of Orion? 21 | 22 | [1] Altair 23 | [2] Betelgeuse 24 | [3] Aldebaran 25 | [4] Andromeda 26 | """, 27 | "SIRIUS_QUIZ_CORRECT": """ 28 | *Correct!!!* You win a ton or credits. 29 | """, 30 | "SIRIUS_QUIZ_INCORRECT": """ 31 | Sorry, this was the wrong answer. Don't take it too sirius. 32 | Better luck next time. 33 | """, 34 | "ORION_DESCRIPTION": """ 35 | You are on Orion. An icy world inhabited by furry sentients.""", 36 | "ORION_HIRE_COPILOT_QUESTION": """A tech-savvy native admires your spaceship. 37 | They promise to join as a copilot if you can answer a question: 38 | 39 | What is the answer to question of life, the universe and everything? 40 | 41 | What do you answer?""", 42 | "COPILOT_QUESTION_CORRECT": """ 43 | Your new copilot jumps on board and immediately starts 44 | configuring new docker containers. 45 | """, 46 | "COPILOT_QUESTION_INCORRECT": """ 47 | Sorry, that's not it. Try again later. 48 | """, 49 | "BLACK_HOLE_DESCRIPTION": """ 50 | You are close to Black Hole #0997. Maybe coming here was a really stupid idea. 51 | Do you want to examine the black hole closer? [yes/no] 52 | """, 53 | "BLACK_HOLE_CRUNCHED": """ 54 | The black hole condenses your spaceship into a grain of dust. 55 | 56 | THE END 57 | """, 58 | "BLACK_HOLE_COPILOT_SAVES_YOU": """ 59 | On the rim of the black hole your copilot blurts out: 60 | 61 | Turn left! 62 | 63 | You ignite the next-gen hyperdrive, creating a time-space anomaly. 64 | You travel through other dimensions and experience wonders beyond description. 65 | """, 66 | "END_CREDITS": """ 67 | THE END 68 | """, 69 | } 70 | -------------------------------------------------------------------------------- /space_game.py: -------------------------------------------------------------------------------- 1 | """ 2 | Space Travel Game 3 | 4 | A simple text adventure written for a refactoring tutorial. 5 | """ 6 | 7 | TEXT = { 8 | "OPENING_MESSAGE": """ 9 | ------------------------------------------------------------------------------- 10 | 11 | You and your trusted spaceship set out to look for 12 | fame, wisdom, and adventure. The stars are waiting for you. 13 | """, 14 | "EARTH_DESCRIPTION": "\nYou are on Earth. Beautiful is better than ugly.", 15 | "CENTAURI_DESCRIPTION": "\nYou are on Alpha Centauri. All creatures are welcome here.", 16 | "HYPERDRIVE_SHOPPING_QUESTION": """There is a brand new hyperdrive with a built-in GPU for sale. 17 | 18 | Would you like to buy one [yes/no]""", 19 | "HYPERDRIVE_TOO_EXPENSIVE": """ 20 | You cannot afford it. The GPU is too expensive.""", 21 | "SIRIUS_DESCRIPTION": """ 22 | You are on Sirius. The system is full of media companies and content delivery networks.""", 23 | "SIRIUS_QUIZ_QUESTION": """You manage to get a place in *Stellar* - the greatest quiz show in the universe. 24 | Here is your question: 25 | 26 | Which star do you find on the shoulder of Orion? 27 | 28 | [1] Altair 29 | [2] Betelgeuse 30 | [3] Aldebaran 31 | [4] Andromeda 32 | """, 33 | "SIRIUS_QUIZ_CORRECT": """ 34 | *Correct!!!* You win a ton or credits. 35 | """, 36 | "SIRIUS_QUIZ_INCORRECT": """ 37 | Sorry, this was the wrong answer. Don't take it too sirius. 38 | Better luck next time. 39 | """, 40 | "ORION_DESCRIPTION": """ 41 | You are on Orion. An icy world inhabited by furry sentients.""", 42 | "ORION_HIRE_COPILOT_QUESTION": """A tech-savvy native admires your spaceship. 43 | They promise to join as a copilot if you can answer a question: 44 | 45 | What is the answer to question of life, the universe and everything? 46 | 47 | What do you answer?""", 48 | "COPILOT_QUESTION_CORRECT": """ 49 | Your new copilot jumps on board and immediately starts 50 | configuring new docker containers. 51 | """, 52 | "COPILOT_QUESTION_INCORRECT": """ 53 | Sorry, that's not it. Try again later. 54 | """, 55 | "BLACK_HOLE_DESCRIPTION": """ 56 | You are close to Black Hole #0997. Maybe coming here was a really stupid idea. 57 | Do you want to examine the black hole closer? [yes/no] 58 | """, 59 | "BLACK_HOLE_CRUNCHED": """ 60 | The black hole condenses your spaceship into a grain of dust. 61 | 62 | THE END 63 | """, 64 | "BLACK_HOLE_COPILOT_SAVES_YOU": """ 65 | On the rim of the black hole your copilot blurts out: 66 | 67 | Turn left! 68 | 69 | You ignite the next-gen hyperdrive, creating a time-space anomaly. 70 | You travel through other dimensions and experience wonders beyond description. 71 | """, 72 | "END_CREDITS": """ 73 | THE END 74 | """, 75 | } 76 | 77 | 78 | def travel(): 79 | print(TEXT["OPENING_MESSAGE"]) 80 | 81 | planet = "earth" 82 | engines = False 83 | copilot = False 84 | credits = False 85 | game_end = False 86 | 87 | while not game_end: 88 | # display inventory 89 | print("-" * 79) 90 | inventory = "\nYou have: " 91 | inventory += "plenty of credits, " if credits else "" 92 | inventory += "a hyperdrive, " if engines else "" 93 | inventory += "a skilled copilot, " if copilot else "" 94 | if inventory.endswith(", "): 95 | print(inventory.strip(", ")) 96 | 97 | # 98 | # interaction with planets 99 | # 100 | if planet == "earth": 101 | destinations = ["centauri", "sirius"] 102 | print(TEXT["EARTH_DESCRIPTION"]) 103 | 104 | if planet == "centauri": 105 | print(TEXT["CENTAURI_DESCRIPTION"]) 106 | destinations = ["earth", "orion"] 107 | 108 | if not engines: 109 | print(TEXT["HYPERDRIVE_SHOPPING_QUESTION"]) 110 | if input() == "yes": 111 | if credits: 112 | engines = True 113 | else: 114 | print(TEXT["HYPERDRIVE_TOO_EXPENSIVE"]) 115 | 116 | if planet == "sirius": 117 | print(TEXT["SIRIUS_DESCRIPTION"]) 118 | destinations = ["orion", "earth", "black_hole"] 119 | 120 | if not credits: 121 | print(TEXT["SIRIUS_QUIZ_QUESTION"]) 122 | answer = input() 123 | if answer == "2": 124 | print(TEXT["SIRIUS_QUIZ_CORRECT"]) 125 | credits = True 126 | else: 127 | print(TEXT["SIRIUS_QUIZ_INCORRECT"]) 128 | 129 | if planet == "orion": 130 | destinations = ["centauri", "sirius"] 131 | if not copilot: 132 | print(TEXT["ORION_DESCRIPTION"]) 133 | print(TEXT["ORION_HIRE_COPILOT_QUESTION"]) 134 | if input() == "42": 135 | print(TEXT["COPILOT_QUESTION_CORRECT"]) 136 | copilot = True 137 | else: 138 | print(TEXT["COPILOT_QUESTION_INCORRECT"]) 139 | else: 140 | print(TEXT["ORION_DESCRIPTION"]) 141 | 142 | if planet == "black_hole": 143 | print(TEXT["BLACK_HOLE_DESCRIPTION"]) 144 | destinations = ["sirius"] 145 | if input() == "yes": 146 | if engines and copilot: 147 | print(TEXT["BLACK_HOLE_COPILOT_SAVES_YOU"]) 148 | game_end = True 149 | else: 150 | print(TEXT["BLACK_HOLE_CRUNCHED"]) 151 | return 152 | 153 | if not game_end: 154 | # select next planet 155 | print("\nWhere do you want to travel?") 156 | position = 1 157 | for d in destinations: 158 | print(f"[{position}] {d}") 159 | position += 1 160 | 161 | choice = input() 162 | planet = destinations[int(choice) - 1] 163 | 164 | print(TEXT["END_CREDITS"]) 165 | 166 | 167 | if __name__ == "__main__": 168 | travel() 169 | -------------------------------------------------------------------------------- /talk/PRESENT.md: -------------------------------------------------------------------------------- 1 | 2 | # Refactoring 3 | 4 | Dr. Kristian Rother 5 | 6 | [www.academis.eu](https://www.academis.eu) 7 | 8 | ![](four_icon_cv.png) 9 | 10 | 11 | ---- 12 | 13 | ## Agenda 14 | 15 | - setup 16 | - what is refactoring? 17 | - find problems 18 | - extract a module 19 | - extract functions 20 | - extract data structures 21 | - extract a class 22 | 23 | ---- 24 | 25 | ## Space Game - Setup 26 | 27 | ![](starmap.png) 28 | 29 | *planet images by [Justin Nichol](https://opengameart.org/content/20-planet-sprites) CC-BY 3.0* 30 | 31 | github.com/krother/refactoring_tutorial 32 | 33 | ### Run tests 34 | 35 | pip install pytest 36 | pytest test_space_game.py 37 | 38 | ### Play 39 | 40 | python space_game.py 41 | 42 | 43 | ---- 44 | 45 | ## What is Refactoring? 46 | 47 | Improve the structure of code without changing its functionality. 48 | 49 | Refactoring is like washing. 50 | It is most effective if repeated regularly. 51 | 52 | 53 | ### Generic Recipe 54 | 55 | 1. run the tests 56 | 2. improve the code 57 | 3. run the tests 58 | 59 | ---- 60 | 61 | ## Identify problems 62 | 63 | Look for the following in `space_game.py`: 64 | 65 | - long modules 66 | - long functions 67 | - duplications 68 | - similar sections 69 | - excessive indentation 70 | - bad names 71 | - mix of languages 72 | - mix of domains (UI + business logic) 73 | - too complex code 74 | - code that you find hard to read 75 | 76 | ---- 77 | 78 | ## Extract a Module 79 | 80 | Move `TEXT` to a separate file. 81 | 82 | 1. create an empty Python file `text_en.py` 83 | 2. cut and paste the entire dictionary `TEXT` 84 | 3. add an import `from text_en import TEXT` 85 | 4. run the tests again 86 | 87 | ---- 88 | 89 | ## Extract Functions 90 | 91 | Make shorter functions out of long ones. 92 | 93 | ### Recipe 94 | 95 | 1. select a code section 96 | 2. name the function 97 | 3. indent the code 98 | 4. make a parameter out of every variable 99 | not created inside the function 100 | 5. Add a return statement 101 | 6. Add a function call 102 | 7. Run the tests 103 | 104 | ---- 105 | 106 | ## Exercise 1: display_inventory 107 | 108 | Extract a function from the paragraph labeled **display inventory**. 109 | Use the signature: 110 | 111 | def display_inventory(credits, engines, copilot) 112 | 113 | This function does not need a return statement. 114 | 115 | ---- 116 | 117 | ## Exercise 2: select_planet 118 | 119 | Extract a function `select_planet()` from the last code paragraph in `travel()`. 120 | 121 | * the function needs a single parameter 122 | * the function needs a single return value 123 | 124 | Work through the recipe. 125 | 126 | ---- 127 | 128 | ## Extract and Modify 129 | 130 | Extract a function `visit_planets()`. 131 | Use the signature: 132 | 133 | def visit_planet(planet, engines, copilot, credits, game_end): 134 | ... 135 | 136 | and the function call: 137 | 138 | destinations, engines, copilot, credits, game_end = \ 139 | visit_planet(planet, engines, copilot, credits, game_end) 140 | 141 | **The tests should fail!** 142 | 143 | ---- 144 | 145 | ## Fix the tests 146 | 147 | Fix two problems: 148 | 149 | The extra `return` statement in the black hole section). 150 | 151 | Replace the `return` statement in the black hole section 152 | by `game_end = True` 153 | 154 | Move the line printing end credits into the conditional branch where your copilot saves you. 155 | 156 | ---- 157 | 158 | ## How small should functions be? 159 | 160 | Uncle Bob (Robert C. Martin) states: 161 | 162 | Each function should do exactly one thing 163 | 164 | Q: When is a function doing exactly one thing? 165 | 166 | A: When you cannot make two functions out of it. 167 | 168 | ---- 169 | 170 | ## Extract Data Structures 171 | 172 | Convert the boolean variables: `copilot`, `credits`, `engine` and `game_end` into a data structure. 173 | 174 | Define an empty set: 175 | 176 | flags = set() 177 | 178 | Assign the posible flag values somewhere on top: 179 | 180 | copilot, credits, engine, game_end = range(4) 181 | 182 | Change the `while` loop: 183 | 184 | while not game_end in flags: 185 | 186 | Modify `display_inventory()`: 187 | 188 | 1. replace the function arguments by `flags` 189 | 2. modify the function call 190 | 3. to check state, use `if credits in flags:` 191 | 192 | Modify `visit_planet()`: 193 | 194 | 1. replace the boolean arguments by `flags` 195 | 2. modify the function call 196 | 3. only return `planet` and `destinations` 197 | 4. modify the assigned return in `travel()` 198 | 5. to check state, use `if credits in flags:` 199 | 6. to modify state, use `flags.add('crystal_found')` 200 | 201 | Run the tests. 202 | 203 | ---- 204 | 205 | ## Extract puzzle functions 206 | 207 | Decompose `visit_planet()` into further functions: 208 | 209 | if planet == "centauri": 210 | print(TEXT["CENTAURI_DESCRIPTION"]) 211 | destinations = ["earth", "orion"] 212 | buy_hyperdrive(flags) 213 | 214 | Do the same for the other puzzles: 215 | 216 | def star_quiz(flags): 217 | 218 | def hire_copilot(flags): 219 | 220 | def black_hole(flags): 221 | 222 | Now `visit_planet()` should fit on your screen. 223 | 224 | ## Extract a dictionary 225 | 226 | Place the destinations in a data structure: 227 | 228 | STARMAP = { 229 | 'earth': ['centauri', 'sirius'], 230 | 'centauri': ['earth', 'orion'], 231 | 'sirius': ..., 232 | 'orion': ..., 233 | 'black_hole': ['sirius'], 234 | } 235 | 236 | 1. place the dictionary on top of the file 237 | 2. fill the missing positions 238 | 3. remove the `destinations` from `visit_planet()` 239 | 4. instead, return destinations with `return STARMAP[planet]` 240 | 5. run the tests 241 | 242 | 243 | ---- 244 | 245 | ## More dictionaries? 246 | 247 | Maybe we should extract the descriptions of each planet: 248 | 249 | PLANET_DESCRIPTIONS = { 250 | 'earth': TEXT['EARTH_DESCRIPTION], 251 | 'sirius': TEXT['SIRIUS_DESCRIPTION], 252 | ... 253 | } 254 | 255 | Is this a good idea? 256 | 257 | ## Extract a Planet class 258 | 259 | Define a new class `Planet`: 260 | 261 | class Planet: 262 | 263 | def __init__(self, name, description connections): 264 | self.name = name 265 | self.description = description 266 | self.connections = connections 267 | 268 | 269 | ### Add a method 270 | 271 | Convert the function `visit_planet()` into a method: 272 | 273 | def visit(self, flags): 274 | print(self.description) 275 | 276 | The tests will fail. 277 | 278 | ### Create instances 279 | 280 | Create a dictionary of planets. 281 | 282 | PLANETS = { 283 | 'earth': Planet( 284 | 'earth', 285 | TEXT['EARTH_DESCRIPTION'], 286 | ['centauri', 'sirius'],), 287 | ... 288 | } 289 | 290 | Use the `Planet` instances in `travel()`: 291 | 292 | planet = PLANETS['earth'] 293 | ... 294 | while ...: 295 | planet.visit(flags) 296 | display_destinations(planet) 297 | planet = select_planet(planet.destinations) 298 | 299 | Run the tests and make them pass. 300 | 301 | ## Break down the visit function 302 | 303 | There is still a block of `if` statements in `visit()`. 304 | 305 | Add a puzzle attribute to `Planet.__init__()` 306 | 307 | Use the puzzle functions as callbacks. 308 | 309 | 'sirius`: Planet( 310 | 'sirius', 311 | TEXT['SIRIUS_DESCRIPTION'], 312 | star_quiz) 313 | 314 | Now in the `visit()` method, all you need to do is call the callback: 315 | 316 | if puzzle: 317 | puzzle(flags) 318 | 319 | And the multiple `if` statements should evaporate. 320 | 321 | ---- 322 | 323 | ## Notes 324 | 325 | * names matter 326 | * follow a clear paradigm (OOP, functional) 327 | * use Design Patterns 328 | * separate UI / business logic 329 | * anticipate future change 330 | * proceed in small iterations 331 | 332 | In my experience, refactoring is much about executing a few standard techniques consistently. 333 | 334 | You find a great list of refactoring techniques on [refactoring.guru](https://refactoring.guru/) by Alexander Shvets. 335 | 336 | **Give it a try and have fun programming!** 337 | 338 | ---- 339 | 340 | ## License 341 | 342 | (c) 2022 Dr. Kristian Rother `kristian.rother@posteo.de` 343 | 344 | MIT License -------------------------------------------------------------------------------- /talk/abstract.md: -------------------------------------------------------------------------------- 1 | 2 | # Refactoring 101 3 | 4 | *This is the abstract of my 2022 PyCon tutorial.* 5 | 6 | ## Abstract 7 | 8 | In this tutorial, you will refactor a space travel text adventure. 9 | Starting with a working but messy program, you will improve the structure of the code. 10 | You will identify redundant code segments, split long functions into shorter ones, extract data structures and encapsulate behavior into classes. The outcome will be a program that is more readable, easier to maintain, and, hopefully, still works. 11 | 12 | The tutorial will be delivered as a guided tour with many hands-on exercises. 13 | Together, we collect strategies that can be applied to Python projects that grow bigger and bigger. 14 | The refactoring tutorial is suitable for junior Python developers. 15 | 16 | ## Description 17 | 18 | ### Goal of the tutorial: 19 | 20 | As a participant, I want to refactor a piece of unknown Python code, so that you I apply the same techniques to a larger codebase later. 21 | 22 | ### Motivation: 23 | 24 | When you are working on your first real-world Python project, the codebase is typically much larger than any textbook or course example. Over time, software entropy kicks: functions grow longer and longer, the same code gets copy-pasted to multiple places and slowly mutates there, and code that seemed a brilliant idea a few weeks back is now incomprehensible. 25 | 26 | ### Prerequisites: 27 | 28 | You should be well fluent in the basic Python data structures, in particular lists and dictionaries. 29 | You should also have written functions on your own. It helps if you know how a class looks like and how to recognize a generator function. If you have tried writing any of the above and are not happy about the outcome, you have come to the right place. 30 | 31 | The tutorial works with any Python installation >= 3.6. It requires a code editor like PyCharm, VSCode or Spyder (no notebooks). We will use the pytest library. 32 | 33 | The tutorial focuses on programming strategies, so you don't need any advanced elements of the Python language. 34 | In particular, we will ignore type hints, decorators, asyncio, Dataclasses, ABC's, metaclasses or pattern matching even if these are great and would help. You do not need any deep knowledge of particular Python packages. 35 | 36 | ### Structure: 37 | 38 | 1. What is refactoring? 39 | 2. Clone or download the space travel game and make sure it runs 40 | 3. Run the tests 41 | 4. Identify problematic pieces of code using a checklist of "code smells" 42 | 5. Split a long function into multiple shorter ones (apply one of the most fundamental refactoring techniques for warming up) 43 | 6. Extract data structures (2-3 separate examples) 44 | 7. Encapsulate behavior into a class (this will take longer, because we need to discuss pros and cons of different alternatives) 45 | 8. Run the tests again (and emphasize that points 3.+8. are the most important ones on the list; still thinking about special effects here) 46 | 9. Discuss common strategies (functional, class-based and hybrid paradigms, design patterns, testability. 47 | 10. Closing remarks 48 | 49 | The tutorial will be broken down in elementary steps that work both in onsite and online formats. 50 | The complete materials will be available under an Open Source License 51 | 52 | -------------------------------------------------------------------------------- /talk/four_icon_cv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/refactoring_tutorial/b1fff0bf20eae079ffebc34d200e65c2fe943fe0/talk/four_icon_cv.png -------------------------------------------------------------------------------- /talk/starmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/refactoring_tutorial/b1fff0bf20eae079ffebc34d200e65c2fe943fe0/talk/starmap.png -------------------------------------------------------------------------------- /test_space_game.py: -------------------------------------------------------------------------------- 1 | import io 2 | import pytest 3 | 4 | from space_game import travel 5 | 6 | 7 | # the actual solution to the game 8 | SOLUTION = [ 9 | "2", 10 | "2", # go to sirius and win quiz 11 | "1", 12 | "42", # hire copilot on orion 13 | "1", 14 | "yes", # go to centauri and buy GPU drive 15 | "2", 16 | "2", 17 | "3", 18 | "yes", # jump into black hole 19 | ] 20 | 21 | DEATH_BY_BLACK_HOLE = [ 22 | "2", 23 | "2", # go to sirius and win quiz 24 | "1", 25 | "41", # hire copilot on orion 26 | "1", 27 | "yes", # go to centauri and buy GPU drive 28 | "1", 29 | "2", 30 | "3", 31 | "yes", # jump into black hole 32 | ] 33 | 34 | # text sniplets that should appear literally in the output 35 | PHRASES = [ 36 | "The stars are waiting for you", 37 | "Betelgeuse", 38 | "credits", 39 | "tech-savvy native", 40 | "copilot", 41 | "buy", 42 | "life, the universe and everything", 43 | "Black Hole", 44 | "stupid idea", 45 | "wonders beyond description", 46 | "THE END", 47 | ] 48 | 49 | 50 | @pytest.fixture 51 | def solution_input(): 52 | """helper function to hijack the keyboard for testing""" 53 | return io.StringIO("\n".join(SOLUTION)) 54 | 55 | 56 | def test_travel(monkeypatch, solution_input): 57 | """game finishes""" 58 | monkeypatch.setattr("sys.stdin", solution_input) 59 | travel() 60 | 61 | 62 | def test_output(monkeypatch, capsys, solution_input): 63 | """text output is not empty""" 64 | monkeypatch.setattr("sys.stdin", solution_input) 65 | 66 | travel() 67 | 68 | captured = capsys.readouterr() 69 | assert len(captured.out) > 0 70 | 71 | 72 | def test_die(monkeypatch, capsys): 73 | """player dies""" 74 | monkeypatch.setattr("sys.stdin", io.StringIO("\n".join(DEATH_BY_BLACK_HOLE))) 75 | 76 | travel() 77 | 78 | captured = capsys.readouterr() 79 | assert "grain of dust" in captured.out 80 | assert " wonders beyond description" not in captured.out 81 | 82 | 83 | @pytest.mark.parametrize("phrase", PHRASES) 84 | def test_output_phrases(monkeypatch, capsys, solution_input, phrase): 85 | """check for some key phrases in the output""" 86 | monkeypatch.setattr("sys.stdin", solution_input) 87 | 88 | travel() 89 | 90 | captured = capsys.readouterr() 91 | assert phrase in captured.out 92 | --------------------------------------------------------------------------------