├── .coveragerc ├── .gitignore ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── LICENSE ├── Pipfile ├── Pipfile.lock ├── README.md ├── TODO.md ├── TODO.org ├── examples └── testgame.py ├── intficpy ├── __init__.py ├── actor.py ├── cli.py ├── daemons.py ├── event.py ├── exceptions.py ├── grammar.py ├── ifp_game.py ├── ifp_object.py ├── parser.py ├── physical_entity.py ├── room.py ├── score.py ├── sequence.py ├── serializer.py ├── thing_base.py ├── things.py ├── tokenizer.py ├── tools.py ├── travel.py ├── verb.py └── vocab.py ├── pre-commit ├── scripts └── rule_checker.py ├── setup.py └── tests ├── __init__.py ├── helpers.py ├── test_desc.py ├── test_event.py ├── test_game.py ├── test_info_commands.py ├── test_parser.py ├── test_score.py ├── test_sequence.py ├── test_serializer.py ├── test_travel.py ├── things ├── __init__.py ├── test_actor.py ├── test_copy_thing.py ├── test_light_source.py ├── test_nested_things.py └── test_things.py └── verbs ├── __init__.py ├── test_buy_sell.py ├── test_clothing_verbs.py ├── test_conversation.py ├── test_drop_verb.py ├── test_get.py ├── test_get_all_drop_all.py ├── test_help_and_score.py ├── test_hint.py ├── test_inventory.py ├── test_light_verbs.py ├── test_liquid_verbs.py ├── test_look.py ├── test_nested_player.py ├── test_open_close_lock.py ├── test_position_verbs.py └── test_set_verbs.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = *tests* 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | __pycache__/ 3 | *.egg-info 4 | htmlcov/ 5 | .coverage 6 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v2.4.0 6 | hooks: 7 | - id: trailing-whitespace 8 | - id: end-of-file-fixer 9 | - id: check-yaml 10 | - id: check-added-large-files 11 | - repo: https://github.com/psf/black 12 | rev: 19.10b0 13 | hooks: 14 | - id: black 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG (since v1.3) 2 | 3 | + moved IFPObject tracking from the IFPObject *base class* to the IFPGame *instance* 4 | 5 | + *all* IFPObject instances now need to be initialised with the game object. 6 | 7 | + verbs are now defined as *subclasses* of verb, not instances 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 JSMaika 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 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | 10 | [requires] 11 | python_version = "3.6" 12 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "415dfdcb118dd9bdfef17671cb7dcd78dbd69b6ae7d4f39e8b44e71d60ca72e7" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.6" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": {}, 19 | "develop": {} 20 | } 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # About IntFicPy 2 | A python library for writing parser-based interactive fiction. Currently in early development. 3 | 4 | [IntFicPy Docs](https://jsmaika.github.io/intficpy-docs/) 5 | 6 | ## Parser-based interactive fiction 7 | Parser based interactive fiction, sometimes called text adventure, is a story displayed through text, that the reader/player interacts with by typing out commands. Typing "get lamp", for instance, will cause the character to take the lamp. 8 | 9 | ## Why IntFicPy? 10 | All of the major systems for writing parser based interactive have their own languages, useful for nothing else. With IntFicPy, I wanted to make it possible for creators who already knew Python to use that knowledge for writing interactive fiction, and for creators new to Python to work in a language for which myriad tutorials, and a strong online community already exist. 11 | 12 | # IntFicPy Engine 13 | IntFicPy is a Python game engine for creating parser-based interactive fiction (text adventures). IntFicPy is designed to be comprehensive and extensible. It has 80 predefined verbs, including all the standards of the genre, many with syonyms (get/take) and alternate phrasings (put hat on/put on hat). Game creators can define their own verbs - which integrate seamlessly with the predefined verb set - in minutes. Built in support for complex conversations with NPCs, dark areas and moveable light sources, locks and keys, save/load, and much more. 14 | ### Parser 15 | Parsing natural language commands can be challenging. For this project, the problem was simplified substantially by the conventions of interactive fiction (IF). Commands are always in the imperative tense (phrased like direct orders). Knowing this, we can guarantee the basic word order of commands. 16 | The IntFicPy parser starts by looking at the first word of the command, which should contain the verb. It then uses clues from the command, like prepositions, and number of grammatical objects to determine which verb function to call. 17 | ### Verbs 18 | At the time of writing, IntFicPy has 78 verbs built in. Users can also create their own verbs, specific to their games. 19 | Each verb in IntFicPy is an instance of the Verb class. When a new instance is created, the verb is automatically added to the game's dictionary of verbs. When synonyms are added using the addSynonm method, they are also added to the dicitonary. 20 | ### Item Classes 21 | IntFicPy currently has 28 classes for items, all subclasses of Thing. Each class of item behaves differently when used for verbs. For instance, you can put a Thing in a Container, read a Readable, unlock a Lock using a Key, or look through a Transparent. 22 | Composite items can be created with the addComposite method, to create, for instance, a box with writing on it, or a dresser with three drawers. 23 | Non Player Characters and Conversations 24 | The class for non-player-characters in IntFicPy is Actor. The most important distinguishing feature of Actors is that the player can talk with them. Creators can either set up responses to Topics ("ask/tell/give/show X (about) Y", where X is an Actor, and Y is an item in the game or creator defined abstract concept), or use SpecialTopics. Special topics allow for a menu style conversation, where the player can select from topics that are suggested to them. 25 | 26 | # Installation and Use 27 | ## First Release & Current Developments 28 | 29 | The first full length IntFicPy game, *Island in the Storm* was entered into IFComp 2019. Many thanks to everyone who played and voted. 30 | 31 | IntFicPy's first release, originally planned for fall 2019, is postponed until further notice, while I improve test coverage and refactor. 32 | 33 | If you want to play around with the library, please do - and I'd love to hear your feedback - but be aware that IntFicPy is not currently stable. 34 | 35 | ## How to Install IntFicPy 36 | 37 | After cloning the repo, install the dependencies, then the IntFicPy package. 38 | 39 | ``` 40 | $ cd intficpy 41 | $ pipenv shell 42 | $ pipenv install 43 | $ pip install -e . 44 | ``` 45 | You should now be able to run the example game. 46 | ``` 47 | $ python examples/testgame.py 48 | ``` 49 | You can also run the tests. 50 | ``` 51 | $ python -m unittest discover 52 | ``` 53 | 54 | # License 55 | IntFicPy is distributed with the MIT license (see LICENSE) 56 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | # Next Goals: 3 | + 100% test coverage for verbs 4 | + standardise creation API across Thing & subclasses 5 | 6 | 7 | # Bugs 8 | + Parser.checkExtra: currently fails to detect nonsensically placed duplicates of words 9 | that are accounted for. Catches `>climb happy on desk` as invalid, but reads 10 | `>climb desk on desk` as equivalent to `>climb on desk`. 11 | 12 | ## Refactoring 13 | 14 | ### Add the ability to store state in a DB instead of in live Python objects 15 | Currently, IFP stores state in live objects in Python while the game is running. It 16 | serializes the objects and dumps to a text file for save/load. 17 | 18 | We want to add the ability to optionally store state in a database instead, while 19 | keeping the current system available, and the basic authors' API more or less 20 | unchanged. 21 | 22 | **Idea:** 2 different "data engines" for IFP. Author can choose which to use. 23 | The data engine dictates the behaviour of `__getattr__` and `__setattr__` on all 24 | IFPObjects in the game. First engine simply gets/sets and maybe tracks changes for undo. 25 | Second engine sets/gets from the db entry for that object, leaving the object itself 26 | unchanged. 27 | 28 | **Idea:** The parser matches input to IFPObjects by querying the DB. When the match succeeds, 29 | we *copy* the starting-state object, using a special method that allows us to set all 30 | attributes from the DB. We pass a special game object in, that lives only as long as this 31 | turn, and contains a reference to the *current user*. Our `__getattr__` override now 32 | only has to perform this same copying process on any IFPObject that is accessed by an 33 | attribute (copy, update from db, *pass along the game instance from the referring IFPObject*, 34 | and return the copy). `__setattr__` needs to record any changes so we can save/batch 35 | apply them to the db at the end of the turn. 36 | 37 | 1. **Auto-register all subclasses of IFPObject** - 38 | IFP will need to be able to reconstruct the object instances from the class, and the 39 | saved attributes. This means that the ORM layer will need to be able to identify & 40 | access the correct class object **even if this class is not part of standard IFP.** 41 | To facilitate this, IFPObject will track its own subclasses. 42 | 43 | 1. **Standardise the instantiation API for all IFPObject subclasses** - 44 | Instantiation kwargs = setting attributes on the instance. You can specify some params 45 | as required (and possibly some attributes as protected?) on the class. 46 | This will make it possible to for the query system to quickly generate needed objects 47 | from the db, as well as just being a much nicer API for humans. 48 | 49 | 1. **Standardise the structure of an IFP project, and create a tool to help authors set it up** - 50 | In the new paradigm, IFPObjects will be instantiated when they are needed (turn-by-turn), 51 | rather than being kept alive over the whole course of the playtime. The starting objects 52 | that the author creates will become, essentially, data migrations. We need to create 53 | an intuitive, standardised project structure that keeps these data migrations separate 54 | from the author's custom classes. 55 | 56 | 1. **Replace the save file with a database** - 57 | Replace the save file with a single-file/embeddable non-relational db. 58 | The save file may eventually become a json dump **of** the db that tracks progress 59 | during play, but maybe this is a useful intermediate step? 60 | 61 | 1. **Update the save system** - 62 | In the old paradigm, the save system is designed to associate each item of its data 63 | to a **live IFPObject instance**. This means that it does not currently save all the 64 | needed to **create** the instance when it is needed (once IFPObject instances cease 65 | to persist between turns.) Most importantly, the current save system lacks a record 66 | of which IFPObject subclass the reconstructed object should be an instance of. 67 | The goal here is to create a save/load system that has all the data needed for both 68 | old and new paradigm saving. 69 | 70 | 1. **Allow authors to explicitly set an entry's key for querying, and prevent duplicates** - 71 | Currently, the keys for all IFPObjects are generated automatically, completely 72 | behind the scenes. In order to give authors an easy way to uniquely identify the 73 | IFPObjects they create in their starting state/data migration code, we will allow 74 | authors to specify their own string key for any of their IFPObjects that they wish, 75 | and auto-generate the key as before otherwise. 76 | 77 | 1. **Build the query system** - 78 | We need to be able to 1) explicitly look up & rehydrate an object using its 79 | specified key, without immediately rehydrating every IFPObject attatched to it, 2) 80 | look up & rehydrate any associated IFPObject seamlessly & automatically when 81 | the attribute is accessed, and 3) keep track of changes made to all rehydrated 82 | objects over their lifetime, so the changes can be saved. 83 | 84 | 1. **Create a migrate tool to create the initial database** - 85 | Find a way to protect this db to prevent modification during play. 86 | 87 | 1. **Create a temporary db on game startup to store state during play** - 88 | Save/load becomes a json dump/load. 89 | 90 | 1. **Initially load the game from the starting state db, not the starting state code** - 91 | Use the query system to load the game from the db. Stop running the starting state 92 | IFPObject generation code during play. 93 | 94 | 1. **Only save changed attributes in the current state db & save files** - 95 | Each save file does not need to keep a copy of the entire game. 96 | 97 | 98 | ### Minor 99 | + pull out printed strings to instance properties 100 | 101 | ## Testing 102 | + make sure new version of inline functions works correctly in terminal mode 103 | + the MORE or m built in inline function needs more testing and possible refining 104 | + Abstract class - try breaking it with features that shouldn't be used 105 | + inline functions with multiple arguments 106 | + give thing with give enabled 107 | 108 | ### Things 109 | + remove all contents 110 | + test LightSource consume daemon 111 | 112 | ### Test Hints 113 | + test HintSystem pending_daemon 114 | 115 | ### Test Verbs 116 | + movement (climb, exit, enter, lead) 117 | + positions (stand, sit, lie down) 118 | + liquids (pour, fill, drink) 119 | + press, push, 120 | + light sources (light, extinguish) 121 | + clothing (wear, doff) 122 | + score & fullscore 123 | + help, about, verbs, herb help, hint, instructions 124 | + record & playback (record on, record off, playback) 125 | + built-in placeholders (jump, kill, kick, break) 126 | -------------------------------------------------------------------------------- /TODO.org: -------------------------------------------------------------------------------- 1 | * Stability 2 | ** TODO near 100% coverage for everything *except* verbs 3 | ** TODO improve test coverage for verbs 4 | * Sequence 5 | ** TODO 100% test coverage for sequence.py 6 | ** TODO convert to list only templates (prerequisite for dynamic add/drop) 7 | ** TODO dynamic option add/drop 8 | ** TODO pop control item 9 | ** TODO [BREAKING] any long text (ex room, item descriptions) can be a Sequence 10 | * Serialization Refactor Preparations 11 | ** TODO ability to pass in any/all customizable IFPObject attributes at initialization 12 | -------------------------------------------------------------------------------- /examples/testgame.py: -------------------------------------------------------------------------------- 1 | from intficpy.room import Room, OutdoorRoom 2 | from intficpy.things import ( 3 | Surface, 4 | Container, 5 | Clothing, 6 | Abstract, 7 | Key, 8 | Lock, 9 | UnderSpace, 10 | ) 11 | from intficpy.thing_base import Thing 12 | from intficpy.score import Achievement, Ending 13 | from intficpy.travel import ( 14 | TravelConnector, 15 | DoorConnector, 16 | LadderConnector, 17 | StaircaseConnector, 18 | ) 19 | from intficpy.actor import Actor, Player, Topic, SpecialTopic 20 | from intficpy.ifp_game import IFPGame 21 | from intficpy.cli import TerminalApp 22 | 23 | ex = TerminalApp() 24 | game = IFPGame(ex) 25 | me = Player(game) 26 | game.setPlayer(me) 27 | 28 | # Set the basic game information 29 | game.aboutGame.title = "Demo Game" 30 | game.aboutGame.author = "JSMaika" 31 | 32 | # OPENING 33 | def opening(game): 34 | # Text is fed to to App to be shown to the player in bundles called "events". 35 | # All events are printed at the end of the turn, ordered by their priority, from 36 | # lowest number, to highest. 37 | # Every turn will have 2 events by default - the "turn" event, where IFP puts the 38 | # player's actions and their outcomes, and the "command" event, where the player 39 | # command is echoed 40 | # To add text to the next turn, can, 1) add text to an existing event, with 41 | # game.addTextToEvent("some event name", "New text to append!") 42 | # or, its shortcut for adding to the "turn" event 43 | # game.addText("We'll tack this onto the turn.") 44 | # or, (2) you can add a new, named event, as shown below. 45 | game.addEvent( 46 | "opening", # the name of your event 47 | 1, # the priority of the event. lower numbers will show first. the main turn event is 5 48 | ( 49 | f"{game.aboutGame.title}

" 50 | "You can hear the waves crashing on the shore outside. " 51 | "There are no car sounds, no human voices. " 52 | "You are far from any populated area. " 53 | ), 54 | ) 55 | 56 | 57 | game.gameOpening = opening # this function will be called once when the game begins 58 | 59 | 60 | # ACHIEVEMENTS 61 | opalAchievement = Achievement(game, 2, "finding the opal") 62 | keyAchievement = Achievement(game, 2, "receiving the key") 63 | 64 | # ENDINGS 65 | freeEnding = Ending( 66 | game, 67 | True, # this is a good ending 68 | "**YOU ARE FREE**", 69 | "You arrive on the seashore, leaving Sarah to cower in her shack. You are free.", 70 | ) 71 | 72 | # PLOT ESSENTIAL PROPS 73 | silverkey = Key(game) 74 | silverkey.setAdjectives(["silver"]) 75 | 76 | rustykey = Key(game) 77 | rustykey.setAdjectives(["rusty"]) 78 | 79 | opal = Thing(game, "opal") 80 | opal.makeUnique() 81 | opal.size = 25 82 | 83 | # ROOMS 84 | 85 | # Start Room (Shack Interior) 86 | startroom = Room( 87 | game, 88 | "Shack interior", 89 | "You are standing in a one room shack. Light filters in through a cracked, dirty window. ", 90 | ) 91 | 92 | me.moveTo(startroom) 93 | 94 | # ABSTRACT CONCEPTS 95 | # Use "Abstract" items to create topics of discussion (for ask/tell Topics) that do not 96 | # correlate to a physical item. Alternately, use them to track player knowledge 97 | storm_concept = Abstract(game, "storm") 98 | shack_concept = Abstract(game, "shack") 99 | shack_concept.setAdjectives(["one", "room"]) 100 | shack_concept.makeKnown(me) 101 | 102 | 103 | def takeOpalFunc(game): 104 | if not me.opaltaken: 105 | game.addText( 106 | "As you hold the opal in your hand, you're half-sure you can feel the air cooling around you. A shiver runs down your spine. Something is not right here.", 107 | ) 108 | me.opaltaken = True 109 | opalAchievement.award(game) 110 | print(opal.ix) 111 | print(opal.ix in me.knows_about) 112 | 113 | 114 | opal.getVerbDobj = takeOpalFunc 115 | 116 | bench = Surface(game, "bench") 117 | bench.can_contain_sitting_player = True 118 | bench.can_contain_standing_player = True 119 | bench.invItem = False 120 | bench.description = "A rough wooden bench sits against the wall. " 121 | bench.x_description = ( 122 | "The wooden bench is splintering, and faded grey. It looks very old. " 123 | ) 124 | bench.moveTo(startroom) 125 | 126 | underbench = UnderSpace(game, "space") 127 | underbench.contains_preposition = "in" 128 | bench.addComposite(underbench) 129 | # UnderSpaces that are components of another item are not described if their "description" 130 | # attribute is None 131 | # This also means that we cannot see what's underneath 132 | # Set description to an empty string so we can show what's underneath without 133 | # having to print an actual description of the UnderSpace 134 | underbench.description = "" 135 | underbench.full_name = "space under the bench" 136 | 137 | box = Container(game, "box") 138 | box.giveLid() 139 | box.moveTo(underbench) 140 | 141 | opal.moveTo(box) 142 | 143 | boxlock = Lock(game, True, silverkey) 144 | box.setLock(boxlock) 145 | 146 | # Beach 147 | beach = OutdoorRoom( 148 | game, "Beach, near the shack", "You find yourself on an abandoned beach. " 149 | ) 150 | 151 | 152 | def beachArrival(game): 153 | freeEnding.endGame(game) 154 | 155 | 156 | beach.arriveFunc = beachArrival 157 | 158 | shackdoor = DoorConnector(game, startroom, "e", beach, "w") 159 | shackdoor.entrance_a.description = "To the east, a door leads outside. " 160 | shackdoor.entrance_b.description = "The door to the shack is directly west of you. " 161 | 162 | cabinlock = Lock(game, True, rustykey) 163 | shackdoor.setLock(cabinlock) 164 | 165 | startroom.exit = shackdoor 166 | beach.entrance = shackdoor 167 | 168 | # Attic 169 | 170 | attic = Room(game, "Shack, attic", "You are in a dim, cramped attic. ") 171 | shackladder = LadderConnector(game, startroom, attic) 172 | shackladder.entrance_a.description = ( 173 | "Against the north wall is a ladder leading up to the attic. " 174 | ) 175 | startroom.north = shackladder 176 | silverkey.moveTo(attic) 177 | 178 | # CHARACTERS 179 | # Sarah 180 | sarah = Actor(game, "Sarah") 181 | sarah.makeProper("Sarah") 182 | sarah.moveTo(startroom) 183 | 184 | 185 | def sarahOpalFunc(game, dobj): 186 | """ 187 | IntFicPy verbs can be overridden or customized for specific items. 188 | To do this, find the verb you want to customize in the intficpy.verb module. 189 | Note 190 | 1) the class name of the verb 191 | 2) whether its verbFunc method takes just a "dobj" (direct object) parameter, 192 | or an "iobj" (indirect object) as well 193 | For instance, GetVerb (as in, "get opal") takes only a direct object (here, "opal"), 194 | while GiveVerb (as in, "give opal to Sarah") takes an indirect object as well 195 | (here, "Sarah") 196 | Decide whether you want to override the verb for the direct, or indirect object. 197 | Create a new function in your game file. The first parameter will always be "game". 198 | If you are overriding for an indirect object, you will also need to accept a "dobj" 199 | parameter for the indirect object. Likewise, if you are overriding for the direct 200 | object of a verb that has an indirect object as well, you should accept an "iobj" 201 | parameter to receive the indirect object. 202 | 203 | Inside the function, you can perform whatever tasks you want to do to create your 204 | custom behaviour. 205 | 206 | Return True to skip the verb's normal behaviour after evaluating, or False to continue 207 | as normal. 208 | """ 209 | if not sarah.threwkey and dobj == sarah: 210 | game.addText( 211 | '"Fine!" she cries. "Fine! Take the key and leave! "' 212 | '"Just get that thing away from me!" ', 213 | ) 214 | game.addText("Sarah flings a rusty key at you. You catch it.") 215 | rustykey.moveTo(me) 216 | keyAchievement.award(game) 217 | sarah.threwkey = True 218 | return True 219 | 220 | 221 | # Now that we've created our custom behaviour, we need to hook it up to evaluate when 222 | # the verb is called on our direct or indirect object 223 | # When creating the override function, we should have noted to class name of the IFP 224 | # verb we're overriding. 225 | # In this case, we're creating an override for GiveVerb and ShowVerb for the 226 | # indirect object of the opal 227 | # To do this, we will need to add an attribute to the opal for each verb we are overriding. 228 | # The verb looks for overrides by chacking its objects for attributes with the following 229 | # pattern: 230 | # [firstLetterLoweredClassNameOfVerb]Iobj 231 | # or for direct object overrides: 232 | # [firstLetterLoweredClassNameOfVerb]Dobj 233 | # so, for the indirect object for GiveVerb, we get the attribute, "giveVerbDobj" 234 | # All we need to do now is assign our new function to the attribute name we derived 235 | opal.giveVerbIobj = sarahOpalFunc 236 | opal.showVerbIobj = sarahOpalFunc 237 | 238 | howgethere = SpecialTopic( 239 | game, 240 | "ask how you got here", 241 | 'You ask Sarah how you got here. She bites her lip.
"There was a storm," she says. "Your ship crashed on shore. I brought you inside."', 242 | ) 243 | 244 | 245 | def sarahDefault(game): 246 | game.addText(sarah.default_topic) 247 | storm_concept.makeKnown(me) 248 | sarah.addSpecialTopic(howgethere) 249 | 250 | 251 | sarah.defaultTopic = sarahDefault 252 | 253 | opalTopic = Topic( 254 | game, 255 | '"I should never it from the cave," says Sarah. "I want nothing to do with it. If you\'re smart, you\'ll leave it where you found it."', 256 | ) 257 | sarah.addTopic("asktell", opalTopic, opal) 258 | 259 | shackTopic = Topic( 260 | game, '"It\'s not such a bad place, this shack," Sarah says. "It\'s warm. Safe."' 261 | ) 262 | sarah.addTopic("asktell", shackTopic, shack_concept) 263 | 264 | key2Topic = Topic( 265 | game, 266 | '"Leave that be," Sarah says, a hint of anxiety in her voice. "Some things are better left untouched."', 267 | ) 268 | sarah.addTopic("asktellgiveshow", key2Topic, silverkey) 269 | 270 | sarahTopic = Topic( 271 | game, 272 | '"You want to know about me?" Sarah says. "I\'m flattered. Just think of me as your protector."', 273 | ) 274 | sarah.addTopic("asktell", sarahTopic, sarah) 275 | 276 | meTopic = Topic( 277 | game, 278 | '"You were in a sorry state when I found you," Sarah says. "Be glad I brought you here."', 279 | ) 280 | sarah.addTopic("asktell", meTopic, me) 281 | 282 | sarah.default_topic = "Sarah scoffs." 283 | 284 | stormTopic = Topic( 285 | game, 'Sarah narrows her eyes. "Yes, there was a storm yesterday." she says.' 286 | ) 287 | sarah.addTopic("ask", stormTopic, storm_concept) 288 | 289 | # PROGRESS TRACKING 290 | # IntFicPy is not able to save global variables, or objects that do not inherit from an IFP 291 | # base class 292 | # However, IFP can save attributes added to IFP objects, such as items or characters 293 | # This is one of the easiest ways to save progress or player choice 294 | me.opaltaken = False 295 | sarah.threwkey = False 296 | 297 | # now that all our objects are set up, run the game 298 | ex.runGame() 299 | -------------------------------------------------------------------------------- /intficpy/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RALWORKS/intficpy/3d0e40e1510d4ee07ae13f0cc4ac36926206cdbe/intficpy/__init__.py -------------------------------------------------------------------------------- /intficpy/cli.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | 4 | from .tokenizer import cleanInput 5 | 6 | 7 | class TerminalApp: 8 | 9 | QUIT_COMMANDS = ["quit", "q"] 10 | 11 | def __init__(self, echo_on=False): 12 | self.game = None # we let the game instance set both game.app and app.game 13 | self.command = None 14 | self.echo_on = echo_on 15 | 16 | def printEventText(self, event): 17 | for t in event.text: 18 | # interpret some basic html tags - br as newline, both b and i as bold text 19 | t = t.replace("
", "\n") 20 | t = re.sub(r"<[ib]>", "\033[1m", t) 21 | t = re.sub(r"<\/[ib]>", "\033[0m", t) 22 | 23 | print(t) 24 | print("\n") 25 | 26 | def saveFilePrompt(self, extension, filetype_desc, msg): 27 | cur_dir = os.getcwd() 28 | print(f"Enter a file to save to (current directory: {cur_dir})") 29 | file_name = input(">") 30 | 31 | if len(file_name) == 0: 32 | return None 33 | 34 | file_path = os.sep.join([cur_dir, file_name]) 35 | 36 | if file_path.endswith(extension): 37 | return file_path 38 | 39 | if not file_path.endswith(extension): 40 | file_path += extension 41 | 42 | return file_path 43 | 44 | def openFilePrompt(self, extension, filetype_desc, msg): 45 | cur_dir = os.getcwd() 46 | print(f"Enter a save file (.sav) to open (current directory: {cur_dir})") 47 | file_name = input(">") 48 | 49 | if not file_name.endswith(".sav"): 50 | print("not a save file") 51 | return None 52 | 53 | file_path = os.sep.join([cur_dir, file_name]) 54 | 55 | return file_path 56 | 57 | def runGame(self): 58 | if not self.game: 59 | raise ValueError( 60 | "Please create a game object, and pass in this app in order to run " 61 | "a game." 62 | ) 63 | 64 | print("\n") 65 | 66 | self.game.initGame() 67 | 68 | while True: 69 | self.command = input(">") 70 | if cleanInput(self.command) in self.QUIT_COMMANDS: 71 | break 72 | self.game.turnMain(self.command) 73 | 74 | print("Goodbye.") 75 | -------------------------------------------------------------------------------- /intficpy/daemons.py: -------------------------------------------------------------------------------- 1 | from intficpy.ifp_object import IFPObject 2 | 3 | 4 | class DaemonManager(IFPObject): 5 | def __init__(self, game): 6 | super().__init__(game) 7 | self.active = [] 8 | 9 | def runAll(self, game): 10 | for daemon in self.active: 11 | daemon.func(game) 12 | 13 | def add(self, daemon): 14 | self.active.append(daemon) 15 | daemon.onAdd() 16 | 17 | def remove(self, daemon): 18 | if daemon in self.active: 19 | self.active.remove(daemon) 20 | daemon.onRemove() 21 | 22 | 23 | class Daemon(IFPObject): 24 | """ 25 | While active, a Daemon's func is run every turn. 26 | Properties added to a Daemon object will be saved/loaded, provided they are 27 | serializable, and can be added so a Daemon can track its own state. 28 | """ 29 | 30 | def __init__(self, game, func): 31 | super().__init__(game) 32 | self.func = func 33 | 34 | def onRemove(self): 35 | pass 36 | 37 | def onAdd(self): 38 | pass 39 | -------------------------------------------------------------------------------- /intficpy/event.py: -------------------------------------------------------------------------------- 1 | class IFPEvent: 2 | def __init__(self, game, priority, text, style): 3 | self.game = game 4 | self.style = style 5 | self.priority = priority 6 | self._text = [] 7 | if text: 8 | self._text.append(text) 9 | 10 | def addSubEvent(self, text=None): 11 | e = IFPEvent(self.game, None, text, None) 12 | self._text.append(e) 13 | return e 14 | 15 | @property 16 | def text(self): 17 | text = [] 18 | for t in self._text: 19 | text += getattr(t, "text", [t]) 20 | return text 21 | -------------------------------------------------------------------------------- /intficpy/exceptions.py: -------------------------------------------------------------------------------- 1 | class Unserializable(Exception): 2 | """ 3 | The item is not serializable by the save system. 4 | """ 5 | 6 | pass 7 | 8 | 9 | class DeserializationError(Exception): 10 | """ 11 | Error deserializing a value during game load 12 | """ 13 | 14 | pass 15 | 16 | 17 | class VerbDefinitionError(Exception): 18 | """ 19 | A verb is defined in an incorrect or inconsistent way 20 | """ 21 | 22 | pass 23 | 24 | 25 | class ParserError(Exception): 26 | """ 27 | Error parsing the player command 28 | """ 29 | 30 | pass 31 | 32 | 33 | class VerbMatchError(ParserError): 34 | """ 35 | No matching verb could be identified from the player input 36 | """ 37 | 38 | pass 39 | 40 | 41 | class ObjectMatchError(ParserError): 42 | """ 43 | No matching IFPObject could be found for either the direct or indirect object 44 | in the player command 45 | """ 46 | 47 | pass 48 | 49 | 50 | class OutOfRange(ParserError): 51 | """ 52 | The specified object is out of range for the current verb 53 | """ 54 | 55 | pass 56 | 57 | 58 | class AbortTurn(Exception): 59 | """ 60 | Abort the current turn. Error message will not be printed. 61 | """ 62 | 63 | pass 64 | 65 | 66 | class NoMatchingSuggestion(Exception): 67 | def __init__(self, query, options, matches): 68 | self.query = query 69 | self.options = options 70 | self.matches = matches 71 | 72 | msg = ( 73 | f"Unable to unambiguaously match a suggestion from options {options} " 74 | f"with query `{query}`. Not excluded: {matches}." 75 | ) 76 | super().__init__(msg) 77 | 78 | 79 | class IFPError(Exception): 80 | pass 81 | -------------------------------------------------------------------------------- /intficpy/grammar.py: -------------------------------------------------------------------------------- 1 | from .tokenizer import cleanInput, tokenize, removeArticles 2 | 3 | 4 | class GrammarObject(object): 5 | def __init__(self, tokens="", target=None): 6 | self.tokens = tokens 7 | self.adjectives = [] 8 | self.entity_matches = [] 9 | self.target = target 10 | 11 | @property 12 | def noun_token(self): 13 | return (self.tokens or [None])[-1] 14 | 15 | 16 | class Command(object): 17 | def __init__(self, input_string=""): 18 | self.input_string = cleanInput(input_string) 19 | self.tokens = tokenize(self.input_string) 20 | 21 | self.verb_matches = [] 22 | self.verb = None 23 | self.verb_form = None 24 | 25 | self.things = [] 26 | self.verb = None 27 | self.dobj = None 28 | self.iobj = None 29 | 30 | self.ambiguous = False 31 | self.ambig_noun = None 32 | self.err = False 33 | 34 | self.disambig_objects = [] 35 | 36 | self.specialTopics = {} 37 | self.sequence = None 38 | 39 | @property 40 | def primary_verb_token(self): 41 | return (self.tokens or [None])[0] 42 | 43 | @property 44 | def ambig_obj(self): 45 | if self.dobj and self.dobj.entity_matches and not self.dobj.target: 46 | return dobj 47 | if self.iobj and self.iobj.entity_matches and not self.iobj.target: 48 | return iobj 49 | return None 50 | 51 | @property 52 | def has_active_sequence(self): 53 | return self.sequence and self.sequence.active 54 | 55 | @property 56 | def has_sticky_sequence(self): 57 | return self.has_active_sequence and self.sequence.sticky 58 | -------------------------------------------------------------------------------- /intficpy/ifp_game.py: -------------------------------------------------------------------------------- 1 | from .parser import Parser 2 | from .daemons import DaemonManager 3 | from .score import AbstractScore, HintSystem 4 | from .event import IFPEvent 5 | from .verb import get_base_verbset 6 | 7 | 8 | class GameInfo: 9 | def __init__(self): 10 | self.title = "IntFicPy Game" 11 | self.author = "Unnamed" 12 | self.basic_instructions = ( 13 | "This is a parser-based game, which means the user interacts by typing " 14 | "commands.

A command should be a simple sentence, starting with a " 15 | "verb, for instance,
> JUMP
> TAKE UMBRELLA
> TURN DIAL TO 7" 16 | "

The parser is case insensitive, and ignores punctuation and " 17 | "articles (a, an, the).

It can be difficult at times to figure out " 18 | "the correct verb to perform an action. Even once you have the correct " 19 | "verb, it can be hard to get the right phrasing. In these situations, you " 20 | "can view the full list of verbs in the game using the VERBS command. You " 21 | "can also view the accepted phrasings for a specific verb with the VERB " 22 | "HELP command (for instance, type VERB HELP WEAR).

This game does " 23 | "not have quit and restart commands. To quit, simply close the program. " 24 | "To restart, open it again.

Typing SAVE will open a dialogue to " 25 | "create a save file. Typing LOAD will allow you to select a save file to " 26 | "restore. " 27 | ) 28 | self.game_instructions = None 29 | self.intFicPyCredit = True 30 | self.desc = None 31 | self.showVerbs = True 32 | self.betaTesterCredit = None 33 | self.customMsg = None 34 | self.help_msg = None 35 | 36 | def setInfo(self, title, author): 37 | self.title = title 38 | self.author = author 39 | 40 | def printAbout(self, game): 41 | if self.customMsg: 42 | game.addTextToEvent("turn", self.customMsg) 43 | else: 44 | game.addTextToEvent("turn", "" + self.title + "") 45 | game.addTextToEvent("turn", "Created by " + self.author + "") 46 | if self.intFicPyCredit: 47 | game.addTextToEvent("turn", "Built with JSMaika's IntFicPy parser") 48 | if self.desc: 49 | game.addTextToEvent("turn", self.desc) 50 | if self.betaTesterCredit: 51 | game.addTextToEvent("turn", "Beta Testing Credits") 52 | game.addTextToEvent("turn", self.betaTesterCredit) 53 | 54 | def printInstructions(self, game): 55 | game.addTextToEvent("turn", "Basic Instructions") 56 | game.addTextToEvent("turn", self.basic_instructions) 57 | if self.game_instructions: 58 | game.addTextToEvent("turn", "Game Instructions") 59 | game.addTextToEvent("turn", self.game_instructions) 60 | 61 | def printHelp(self, game): 62 | if self.help_msg: 63 | game.addTextToEvent("turn", self.help_msg) 64 | # self.printVerbs(app) 65 | game.addTextToEvent( 66 | "turn", 67 | "Type INSTRUCTIONS for game instructions, or VERBS for a full list of accepted verbs. ", 68 | ) 69 | 70 | def printVerbs(self, game): 71 | game.addTextToEvent( 72 | "turn", "This game accepts the following basic verbs: " 73 | ) 74 | verb_list = sorted( 75 | set( 76 | [ 77 | verb.list_word or verb.word 78 | for name, verblist in game.verbs.items() 79 | for verb in verblist 80 | if verb.list_by_default 81 | ] 82 | ) 83 | ) 84 | 85 | joined_verb_list = ", ".join(verb_list) + "." 86 | 87 | game.addTextToEvent("turn", joined_verb_list) 88 | 89 | game.addTextToEvent( 90 | "turn", 91 | 'For help with phrasing, type "verb help" followed by a verb for a ' 92 | "complete list of acceptable sentence structures for that verb. This will " 93 | "work for any verb, regardless of whether it has been discovered. ", 94 | ) 95 | 96 | 97 | class IFPGame: 98 | def __init__(self, app, main="__main__"): 99 | # Track the game objects and their vocublary 100 | self.ifp_objects = {} 101 | self.next_obj_ix = 0 102 | self.nouns = {} 103 | self.verbs = get_base_verbset() 104 | 105 | self.app = app 106 | app.game = self 107 | 108 | self.main = __import__(main) 109 | self.aboutGame = GameInfo() 110 | 111 | self.daemons = DaemonManager(self) 112 | self.parser = Parser(self) 113 | 114 | self.ended = False 115 | self.next_events = {} 116 | 117 | self.turn_event_style = None 118 | self.command_event_style = None 119 | self.echo_on = getattr(self.app, "echo_on", True) 120 | 121 | self.recfile = None 122 | self.turn_list = [] 123 | self.back = 0 124 | 125 | self.score = AbstractScore(self) 126 | self.hints = HintSystem(self) 127 | 128 | def runTurnEvents(self): 129 | events = sorted( 130 | [ 131 | event 132 | for name, event in self.next_events.items() 133 | if event.priority is not None 134 | and event._text 135 | and not ((not self.echo_on) and name == "command") 136 | ], 137 | key=lambda x: x.priority, 138 | ) 139 | for event in events: 140 | self.app.printEventText(event) 141 | self.next_events.clear() 142 | self.addEvent("turn", 5, style=self.turn_event_style) 143 | 144 | @staticmethod 145 | def gameOpening(game): 146 | pass 147 | 148 | def initGame(self): 149 | from .things import Abstract 150 | 151 | # HACK: trick the parser into recognizing reflexive pronouns as valid nouns 152 | self.reflexive = Abstract(self, "itself") 153 | self.reflexive.addSynonym("himself") 154 | self.reflexive.addSynonym("herself") 155 | self.reflexive.addSynonym("themself") 156 | self.reflexive.addSynonym("themselves") 157 | self.reflexive.makeKnown(self.me) 158 | 159 | self.addEvent("turn", 5, style=self.turn_event_style) 160 | self.gameOpening(self) 161 | self.parser.roomDescribe() 162 | self.daemons.runAll(self) 163 | self.runTurnEvents() 164 | 165 | def turnMain(self, input_string): 166 | """ 167 | Sends user input to the parser each turn 168 | Runs daemons 169 | Runs turn events 170 | Takes argument input_string, the cleaned user input string 171 | """ 172 | if len(input_string) == 0: 173 | return 0 174 | # parse string 175 | self.parser.parseInput(input_string) 176 | self.daemons.runAll(self) 177 | self.runTurnEvents() 178 | 179 | def addEvent(self, name, priority, text=None, style=None): 180 | """ 181 | Add an event to the current turn 182 | 183 | Raises ValueError if an event of the specified name is already defined 184 | for this turn 185 | """ 186 | if name in self.next_events: 187 | raise ValueError( 188 | f"Cannot add event with name '{name}': " 189 | "Name is already used for current turn." 190 | ) 191 | self.next_events[name] = IFPEvent(self, priority, text, style) 192 | 193 | def addSubEvent(self, outer_name, name, text=None): 194 | """ 195 | Add a sub event to an event on the current turn 196 | 197 | Raises ValueError if an event of the specified name is already defined 198 | for this turn, and KeyError if the outer event is not defined for the 199 | current turn 200 | 201 | :param outer_name: the name of the event to add the sub event into 202 | :type outer_name: str 203 | :param name: the name of the new event (sub event) to add 204 | :type name: str 205 | """ 206 | if not outer_name in self.next_events: 207 | raise KeyError( 208 | f"Cannot add sub event to outer event '{outer_name}': " 209 | "Outer event does not exist in the current turn." 210 | ) 211 | if name in self.next_events: 212 | raise ValueError( 213 | f"Cannot add event with name '{name}': " 214 | "Name is already used for current turn." 215 | ) 216 | self.next_events[name] = self.next_events[outer_name].addSubEvent(text=text) 217 | 218 | def addTextToEvent(self, name, text): 219 | """ 220 | Add text to an event in the current turn 221 | 222 | Raises KeyError if the specified event name is not defined for this 223 | turn 224 | """ 225 | text = self.parser.replace_string_vars(text) 226 | if not name in self.next_events: 227 | raise KeyError( 228 | f"Event with name '{name}' does not yet exist in current turn. " 229 | ) 230 | self.next_events[name]._text.append(text) 231 | 232 | def addText(self, text): 233 | """ 234 | Shortcut to add text to the turn event 235 | """ 236 | self.addTextToEvent("turn", text) 237 | 238 | def recordOn(self, f): 239 | """ 240 | Try opening the specified file for recording, 241 | creating it if it doesn't exist. 242 | """ 243 | try: 244 | recfile = open(f, "w+") 245 | recfile.close() 246 | self.recfile = f 247 | return True 248 | except: 249 | return False 250 | 251 | def recordOff(self): 252 | self.recfile = None 253 | 254 | def getCommandUp(self): 255 | """ 256 | Move backward by 1 through the list of previous commands 257 | Analogous to pressing the Up key in most terminals 258 | """ 259 | if len(self.turn_list) < 1: 260 | return "" 261 | self.back -= 1 262 | if -self.back >= len(self.turn_list): 263 | self.back = 0 264 | return self.turn_list[self.back] 265 | 266 | def getCommandDown(self): 267 | """ 268 | Move forward by 1 through the list of previous commands 269 | Analogous to pressing the Down key in most terminals 270 | """ 271 | if len(self.turn_list) < 1: 272 | return "" 273 | self.back += 1 274 | if self.back >= len(self.turn_list): 275 | self.back = 0 276 | return self.turn_list[self.back] 277 | 278 | def setPlayer(self, player): 279 | self.me = player 280 | self.me.setPlayer() 281 | 282 | def addVerb(self, verb): 283 | for key in [verb.word, *verb.synonyms]: 284 | if key in self.verbs: 285 | self.verbs[key].append(verb) 286 | else: 287 | self.verbs[key] = [verb] 288 | -------------------------------------------------------------------------------- /intficpy/ifp_object.py: -------------------------------------------------------------------------------- 1 | class IFPObject: 2 | def __init__(self, game): 3 | self.game = game 4 | self.registerNewIndex() 5 | self.is_top_level_location = False 6 | 7 | def registerNewIndex(self): 8 | ix = f"{type(self).__name__}__{self.game.next_obj_ix}" 9 | self.ix = ix 10 | self.game.next_obj_ix += 1 11 | self.game.ifp_objects[ix] = self 12 | -------------------------------------------------------------------------------- /intficpy/physical_entity.py: -------------------------------------------------------------------------------- 1 | from .ifp_object import IFPObject 2 | 3 | 4 | class PhysicalEntity(IFPObject): 5 | def __init__(self, game): 6 | super().__init__(game) 7 | 8 | self.location = None 9 | 10 | # CONTENTS 11 | self.contains = {} 12 | self.revealed = True 13 | 14 | def containsItem(self, item): 15 | """Returns True if item is in the contains or sub_contains dictionary """ 16 | return self.topLevelContainsItem(item) or self.subLevelContainsItem(item) 17 | 18 | def topLevelContainsItem(self, item): 19 | if item.ix in self.contains: 20 | if item in self.contains[item.ix]: 21 | return True 22 | return False 23 | 24 | def subLevelContainsItem(self, item): 25 | if item.ix in self.sub_contains: 26 | if item in self.sub_contains[item.ix]: 27 | return True 28 | return False 29 | 30 | def addThing(self, item): 31 | if item.lock_obj and not self.containsItem(item.lock_obj): 32 | self.addTopLevelContains(item.lock_obj) 33 | 34 | for child in item.children: 35 | if not self.containsItem(child): 36 | self.addTopLevelContains(child) 37 | 38 | # top level item 39 | self.addTopLevelContains(item) 40 | 41 | return True 42 | 43 | def addTopLevelContains(self, item): 44 | if item.ix in self.contains: 45 | self.contains[item.ix].append(item) 46 | else: 47 | self.contains[item.ix] = [item] 48 | item.location = self 49 | 50 | def removeThing(self, item): 51 | if not self.containsItem(item): 52 | return False # might be better to raise here 53 | if item.lock_obj and self.containsItem(item.lock_obj): 54 | self.removeContains(item.lock_obj) 55 | 56 | for child in item.children: 57 | if self.containsItem(child): 58 | self.removeContains(child) 59 | 60 | self.removeContains(item) 61 | 62 | if not self.location: 63 | return True 64 | 65 | return True 66 | 67 | def removeContains(self, item): 68 | ret = False # we have not removed anything yet 69 | loc = self 70 | 71 | if self.topLevelContainsItem(item): 72 | self.contains[item.ix].remove(item) 73 | if not self.contains[item.ix]: 74 | del self.contains[item.ix] 75 | item.location = None 76 | return True 77 | 78 | if self.subLevelContainsItem(item): 79 | return item.location.removeThing(item) 80 | 81 | return False 82 | 83 | def getOutermostLocation(self): 84 | if self.location is self: 85 | return self 86 | x = self.location 87 | if not x: 88 | return self.location 89 | while x.location: 90 | x = x.location 91 | return x 92 | 93 | def getNested(self): 94 | """ 95 | Find revealed nested Things 96 | """ 97 | # list to populate with found Things 98 | nested = [] 99 | # iterate through top level contents 100 | if not self.revealed: 101 | return [] 102 | for key in self.contains: 103 | for item in self.contains[key]: 104 | nested.append(item) 105 | for key in self.sub_contains: 106 | for item in self.sub_contains[key]: 107 | nested.append(item) 108 | return nested 109 | 110 | def playerAboutToRemoveItem(self, item, event="turn", **kwargs): 111 | """ 112 | Actions carried out when the player is about to try and remove an item contained 113 | by this item. 114 | 115 | :param event: the event name to print to 116 | :type event: str 117 | """ 118 | return True 119 | 120 | @property 121 | def visible_nested_contents(self): 122 | if not self.revealed: 123 | return [] 124 | ret = self.topLevelContentsList 125 | for item in self.topLevelContentsList: 126 | ret += item.visible_nested_contents 127 | return ret 128 | 129 | @property 130 | def sub_contains(self): 131 | ret = {} 132 | for item in self.topLevelContentsList: 133 | flat = item.visible_nested_contents 134 | for sub in flat: 135 | if sub.ix in ret: 136 | ret[sub.ix].append(sub) 137 | else: 138 | ret[sub.ix] = [sub] 139 | return ret 140 | 141 | @property 142 | def topLevelContentsList(self): 143 | """ 144 | Return the top level contents as a flattened list 145 | 146 | :rtype: list 147 | """ 148 | return [item for ix, sublist in self.contains.items() for item in sublist] 149 | 150 | @property 151 | def subLevelContentsList(self): 152 | """ 153 | Return the sub contents as a flattened list 154 | 155 | :rtype: list 156 | """ 157 | return [item for ix, sublist in self.sub_contains.items() for item in sublist] 158 | 159 | @property 160 | def contentsList(self): 161 | """ 162 | Return the contents from contains and sub_contains as a flattened list 163 | 164 | :rtype: list 165 | """ 166 | return self.topLevelContentsList + self.subLevelContentsList 167 | -------------------------------------------------------------------------------- /intficpy/score.py: -------------------------------------------------------------------------------- 1 | from .ifp_object import IFPObject 2 | from .daemons import Daemon 3 | 4 | ############################################################## 5 | # SCORE.PY - achievements and score for IntFicPy 6 | # Defines the Achievement class, and the Ending class 7 | ############################################################## 8 | 9 | 10 | class Achievement(IFPObject): 11 | """Class for achievements in the .game""" 12 | 13 | def __init__(self, game, points, desc): 14 | super().__init__(game) 15 | self.points = points 16 | self.desc = desc 17 | self.game.score.possible += self.points 18 | 19 | def award(self, game): 20 | # add self to fullscore 21 | if not self in self.game.score.achievements: 22 | game.addTextToEvent( 23 | "turn", 24 | "ACHIEVEMENT:
" 25 | + str(self.points) 26 | + " points for " 27 | + self.desc, 28 | ) 29 | self.game.score.achievements.append(self) 30 | self.game.score.total += self.points 31 | 32 | 33 | class AbstractScore(IFPObject): 34 | def __init__(self, game): 35 | super().__init__(game) 36 | self.total = 0 37 | self.possible = 0 38 | self.achievements = [] 39 | 40 | def score(self, game): 41 | game.addTextToEvent( 42 | "turn", 43 | "You have scored " 44 | + str(self.total) 45 | + " points out of a possible " 46 | + str(self.possible) 47 | + ". ", 48 | ) 49 | 50 | def fullscore(self, game): 51 | if len(self.achievements) == 0: 52 | game.addTextToEvent("turn", "You haven't scored any points so far. ") 53 | else: 54 | game.addTextToEvent("turn", "You have scored: ") 55 | for achievement in self.achievements: 56 | game.addTextToEvent( 57 | "turn", 58 | "" 59 | + str(achievement.points) 60 | + " points for " 61 | + achievement.desc, 62 | ) 63 | 64 | 65 | class Ending(IFPObject): 66 | def __init__(self, game, good, title, desc): 67 | super().__init__(game) 68 | self.good = good 69 | self.title = title 70 | self.desc = desc 71 | 72 | def endGame(self, game): 73 | game.addTextToEvent("turn", "" + self.title + "") 74 | game.addTextToEvent("turn", self.desc) 75 | game.ended = True 76 | 77 | 78 | class HintSystem(IFPObject): 79 | def __init__(self, game): 80 | super().__init__(game) 81 | self.cur_node = None 82 | self.stack = [] 83 | self.pending = [] 84 | self.has_pending_daemon = False 85 | self.pending_daemon = Daemon(self.game, self.checkPending) 86 | 87 | def addPending(self, game, node): 88 | if node not in self.pending: 89 | self.pending.append(node) 90 | if self.pending and not self.has_pending_daemon: 91 | 92 | self.has_pending_daemon = True 93 | if not self.pending_daemon in game.daemons.active: 94 | game.daemons.add(self.pending_daemon) 95 | 96 | def checkPending(self, game): 97 | if self.pending: 98 | remove_nodes = [] 99 | for node in self.pending: 100 | if not node.checkRequiredIncomplete(): 101 | remove_nodes.append(node) 102 | node.complete = True # not sure about this 103 | elif self.setNode(game, node): 104 | remove_nodes.append(node) 105 | for node in remove_nodes: 106 | self.pending.remove(node) 107 | 108 | def setNextNodeFrom(self, game, node): 109 | x = node 110 | if not x: 111 | return False 112 | nodes_checked = [] # record checked nodes to prevent an infinite loop 113 | nodes_checked.append(x) 114 | while x: 115 | if not isinstance(x, HintNode): 116 | raise ValueError(f"{x} is not a HintNode - cannot use as current hint ") 117 | if not x.complete: 118 | if not x.checkRequiredIncomplete(): 119 | x.complete = True # not sure 120 | if x in self.stack: 121 | self.stack.remove(x) 122 | return False 123 | if not x.checkRequiredComplete(): 124 | self.addPending(game, x) 125 | return False 126 | else: 127 | if x not in self.stack: 128 | self.stack.append(x) 129 | self.cur_node = x 130 | return True 131 | x = x.next_node 132 | if x in nodes_checked: 133 | break 134 | nodes_checked.append(x) 135 | return False 136 | 137 | def setNode(self, game, node): 138 | success = self.setNextNodeFrom(game, node) 139 | if not success: 140 | if self.stack: 141 | self.cur_node = self.stack[-1] 142 | else: 143 | self.cur_node = None 144 | return True 145 | 146 | def closeNode(self, game, node): 147 | node.complete = True 148 | if node in self.stack: 149 | self.stack.remove(node) 150 | return self.setNode(game, node) 151 | 152 | 153 | class Hint(IFPObject): 154 | def __init__(self, game, text, achievement=None, cost=0): 155 | super().__init__(game) 156 | self.text = text 157 | self.achievement = achievement 158 | self.cost = cost 159 | self.shown = False 160 | 161 | def giveHint(self, game): 162 | game.addTextToEvent("turn", self.text) 163 | if ( 164 | isinstance(self.achievement, Achievement) 165 | and self.cost > 0 166 | and not self.shown 167 | ): 168 | self.achievement.points -= self.cost 169 | if self.achievement.points < 0: 170 | self.achievement.points = 0 171 | self.shown = True 172 | 173 | 174 | class HintNode(IFPObject): 175 | def __init__(self, game, hints): 176 | super().__init__(game) 177 | self.cur_hint = 0 178 | self.hints = [] 179 | self.next_node = None 180 | self.complete = False 181 | for x in hints: 182 | if not isinstance(x, Hint): 183 | raise ValueError(f"{x} is not a HintNode - cannot add to HintNode") 184 | self.hints = hints 185 | # nodes that must be complete/incomplete in order to open node 186 | self.open_require_nodes_complete = [] 187 | self.open_require_nodes_incomplete = [] 188 | 189 | def checkRequiredComplete(self): 190 | if self.open_require_nodes_complete: 191 | nodes_complete = [ 192 | item.complete for item in self.open_require_nodes_complete 193 | ] 194 | return all(nodes_complete) 195 | return True 196 | 197 | def checkRequiredIncomplete(self): 198 | if self.open_require_nodes_incomplete: 199 | nodes_incomplete = [ 200 | (not item.complete) for item in self.open_require_nodes_incomplete 201 | ] 202 | return all(nodes_incomplete) 203 | return True 204 | 205 | def setHints(self, hints): 206 | for x in hints: 207 | if not isinstance(x, Hint): 208 | raise ValueError(f"{x} is not a HintNode - cannot add to HintNode") 209 | self.hints = hints 210 | 211 | def nextHint(self, game): 212 | """Gives the next hint associated with the HintNode 213 | Returns True if a hint can be given, False on failure """ 214 | 215 | if len(self.hints) == 0: 216 | raise ValueError(f"Cannot use nextHint on {self} - HintNode is empty") 217 | 218 | if self.previousTurnHint(game): 219 | self.cur_hint += 1 220 | if self.cur_hint == len(self.hints): 221 | self.cur_hint -= 1 222 | self.hints[self.cur_hint].giveHint(game) 223 | t = "(Hint tier " + str(self.cur_hint + 1) + "/" + str(len(self.hints)) 224 | if not self.cur_hint < len(self.hints) - 1: 225 | t += ")" 226 | else: 227 | t += " - type hint now to show next)" 228 | game.addTextToEvent("turn", t) 229 | if self.cur_hint < (len(self.hints) - 1): 230 | if ( 231 | not self.hints[self.cur_hint + 1].shown 232 | and self.hints[self.cur_hint + 1].achievement 233 | ): 234 | if self.hints[self.cur_hint + 1].cost == 1: 235 | game.addTextToEvent("turn", "(Next tier costs 1 point)") 236 | else: 237 | game.addTextToEvent( 238 | "turn", 239 | "(Next tier costs " 240 | + str(self.hints[self.cur_hint + 1].cost) 241 | + " points)", 242 | ) 243 | return True 244 | 245 | def previousTurnHint(self, game): 246 | 247 | if len(game.turn_list) < 2: 248 | return False 249 | return game.turn_list[-2] == "hint" 250 | -------------------------------------------------------------------------------- /intficpy/sequence.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | TODO: 4 | * An "Up" ControlItem (pop) 5 | * "Label" ControlItem? - create a more human friendly interface for jumps/procedural navigation 6 | * Conditional logic & flow control - Idea: controller/navigator node where author defines a function (array of functions evaluated in sequence?) to determine where to read next 7 | ``` 8 | # example function to pass to a Navigator 9 | def has_done_thing(sequence): 10 | if sequence.this_was_done: 11 | return [2, 1] 12 | return [2, 3] 13 | ``` 14 | Use in a Sequence template: 15 | ``` 16 | [ 17 | Sequence.Navigator(has_done_thing), 18 | ..., 19 | # or with a labmbda 20 | Sequence.Navigator(lambda seq: [2, 1] if seq.this_was_done else [2, 3]), 21 | ] 22 | ``` 23 | 24 | """ 25 | from inspect import signature 26 | 27 | from .exceptions import IFPError, NoMatchingSuggestion 28 | from .ifp_object import IFPObject 29 | from .tokenizer import cleanInput, tokenize 30 | from .vocab import english 31 | 32 | 33 | class Sequence(IFPObject): 34 | class _Completed(Exception): 35 | pass 36 | 37 | class _Event: 38 | pass 39 | 40 | class _PauseEvent(_Event): 41 | def __init__(self, iterate=False): 42 | self.iterate = iterate 43 | 44 | class _NodeComplete(_Event): 45 | pass 46 | 47 | class ControlItem: 48 | def read(self): 49 | raise NotImplementedError( 50 | "Sequence control classes must define a read method" 51 | ) 52 | 53 | class Prompt(ControlItem): 54 | def __init__(self, save_key, label, question): 55 | self.save_key = save_key 56 | self.label = label 57 | self.question = question 58 | self.answer = None 59 | self.sequence = None 60 | self._submitted = False 61 | 62 | def read(self, game, event="turn"): 63 | if self._submitted: 64 | return 65 | if self.answer: 66 | game.addTextToEvent(event, f"{self.label}: {self.answer}? (y/n)") 67 | else: 68 | game.addTextToEvent(event, self.question) 69 | return self.sequence._PauseEvent() 70 | 71 | def try_again(self): 72 | self.answer = None 73 | self.sequence.play() 74 | 75 | def submit(self): 76 | self.sequence.data[self.save_key] = self.answer 77 | self._submitted = True 78 | self.sequence.play() 79 | self.answer = None 80 | self._submitted = False 81 | 82 | def accept_input(self, tokens): 83 | if self.answer: 84 | if " ".join(tokens) in english.yes: 85 | self.submit() 86 | elif " ".join(tokens) in english.no: 87 | self.try_again() 88 | else: 89 | raise NoMatchingSuggestion( 90 | "Expected yes/no answer", english.yes + english.no, [] 91 | ) 92 | else: 93 | self.answer = " ".join(tokens) 94 | self.sequence.play() 95 | 96 | class SaveData(ControlItem): 97 | def __init__(self, save_key, value): 98 | self.save_key = save_key 99 | self.value = value 100 | self.sequence = None 101 | 102 | def read(self, *args, **kwargs): 103 | self.sequence.data[self.save_key] = self.value 104 | 105 | class Label(ControlItem): 106 | def __init__(self, name): 107 | self.name = name 108 | 109 | def read(self, game, event): 110 | pass # a label does nothing when read 111 | 112 | class Pause(ControlItem): 113 | def read(self, game, event): 114 | return Sequence._PauseEvent(iterate=True) 115 | 116 | class Jump(ControlItem): 117 | """ 118 | Unconditionally jump to a label or index 119 | """ 120 | 121 | def __init__(self, destination): 122 | self.sequence = None 123 | self.destination = destination 124 | 125 | def read(self, game, event): 126 | self.sequence.jump_to(self.destination) 127 | 128 | class Navigator(ControlItem): 129 | def __init__(self, nav_func): 130 | self.nav_func = nav_func 131 | self.sequence = None 132 | 133 | def read(self, game, event): 134 | self.sequence.jump_to(self.nav_func(self.sequence)) 135 | 136 | def __init__(self, game, template, data=None, sticky=False): 137 | super().__init__(game) 138 | self.labels = {} 139 | self._parse_template_node(template) 140 | self.template = template 141 | self.sticky = sticky 142 | 143 | self.position = [0] 144 | self.options = [] 145 | self.data = data or {} 146 | self.data["game"] = game 147 | self.active = False 148 | 149 | self.next_sequence = None 150 | 151 | @property 152 | def current_item(self): 153 | return self._get_section_by_location(self.position) 154 | 155 | @property 156 | def current_node(self): 157 | if len(self.position) < 2: 158 | return self.template 159 | return self._get_section_by_location(self.position[:-1]) 160 | 161 | def start(self): 162 | self.active = True 163 | self.position = [0] 164 | self.play() 165 | 166 | def next(self, event): 167 | ret = self._read_item(self.current_item, event) 168 | if isinstance(ret, self._PauseEvent): 169 | if ret.iterate: 170 | self._iterate() 171 | return ret 172 | return self._iterate() 173 | 174 | def handle_node_complete(self, event): 175 | 176 | if len(self.position) == 1: 177 | self.on_complete() 178 | raise self._Completed() 179 | 180 | self.position = self.position[:-2] # pop out of the list and its parent dict 181 | # self._iterate() 182 | # self.next(event) 183 | 184 | if self.node_ended: 185 | # self._iterate() 186 | # self.next(event) 187 | return self.handle_node_complete(event) 188 | 189 | self.position[-1] += 1 190 | 191 | return self.play(event) 192 | 193 | def play(self, event="turn"): 194 | self.game.parser.command.sequence = self 195 | 196 | cur = self.next(event) 197 | 198 | if isinstance(cur, self._PauseEvent): 199 | return 200 | 201 | if isinstance(cur, self._NodeComplete): 202 | try: 203 | return self.handle_node_complete(event) 204 | except self._Completed: 205 | return 206 | 207 | return self.play(event) 208 | 209 | def jump_to(self, value): 210 | """ 211 | Read the sequence from the specified point. 212 | Accepts a string that is registered as a label on the Sequence 213 | or an array specifying an index on the Sequence template 214 | """ 215 | loc = self._normalize_location(value) 216 | self.position = list(loc) 217 | 218 | def on_complete(self): 219 | self.active = False 220 | if self.next_sequence: 221 | self.next_sequence.start() 222 | 223 | def accept_input(self, tokens): 224 | """ 225 | Pass current input tokens from the parser, to the method corresponding to the 226 | type of input we are currently expecting 227 | """ 228 | if isinstance(self.current_item, self.Prompt): 229 | self.current_item.accept_input(tokens) 230 | else: 231 | self.choose(tokens) # by default, we interpret input as a menu choice 232 | 233 | def choose(self, tokens): 234 | """ 235 | Try to use input tokens from the parser to choose an option in the current menu 236 | """ 237 | ix = None 238 | if len(tokens) == 1: 239 | try: 240 | ix = int(tokens[0]) - 1 241 | except ValueError: 242 | pass 243 | if ix is not None and ix < len(self.options): 244 | self.position += [self.options[ix], 0] 245 | else: 246 | answer = self._match_text_to_suggestion(tokens) 247 | self.position += [answer, 0] 248 | self.play() 249 | 250 | def _match_text_to_suggestion(self, query_tokens): 251 | """ 252 | Try to match tokens to a single suggestion from the current options 253 | Raises NoMatchingSuggestion on failure 254 | """ 255 | tokenized_options = [tokenize(cleanInput(option)) for option in self.options] 256 | match_indeces = [] 257 | 258 | for i in range(len(self.options)): 259 | option = tokenized_options[i] 260 | if not [word for word in query_tokens if not word in option]: 261 | match_indeces.append(i) 262 | 263 | if len(match_indeces) != 1: 264 | raise NoMatchingSuggestion( 265 | query_tokens, self.options, [self.options[ix] for ix in match_indeces] 266 | ) 267 | 268 | return self.options[match_indeces[0]] 269 | 270 | def _get_section_by_location(self, location): 271 | section = self.template 272 | for ix in location: 273 | section = section[ix] 274 | return section 275 | 276 | def _normalize_location(self, value): 277 | if type(value) is str: 278 | try: 279 | loc = self.labels[value] 280 | except KeyError as e: 281 | raise KeyError( 282 | f'"{value}" is not a valid label for Sequence {self}.\n' 283 | f"This Sequence has the following labels: {self.labels.keys()}" 284 | ) from e 285 | return loc 286 | else: 287 | return value 288 | 289 | def _read_item(self, item, event): 290 | self.options = [] 291 | if type(item) is str: 292 | self.game.addTextToEvent(event, item.format(**self.data)) 293 | 294 | elif callable(item): 295 | ret = item(self) 296 | if type(ret) is str: 297 | self.game.addTextToEvent(event, ret) 298 | 299 | elif isinstance(item, self.ControlItem): 300 | item.sequence = self 301 | return item.read(self.game, event) 302 | 303 | else: 304 | self.options = list(item.keys()) 305 | options = "\n".join( 306 | [f"{i + 1}) {self.options[i]}" for i in range(len(self.options))] 307 | ) 308 | self.game.addTextToEvent(event, options) 309 | return self._PauseEvent() 310 | 311 | @property 312 | def node_ended(self): 313 | return self.position[-1] >= len(self.current_node) - 1 314 | 315 | def _iterate(self): 316 | if self.node_ended: 317 | return self._NodeComplete() 318 | self.position[-1] += 1 319 | 320 | def _parse_template_node(self, node, stack=None): 321 | """ 322 | Parse, validate, and prepare the template for reading 323 | """ 324 | stack = stack or [] 325 | 326 | if not type(node) is list: 327 | raise IFPError( 328 | "Expected Sequence node (list); found {node}" f"\nLocation: {stack}" 329 | ) 330 | for i in range(0, len(node)): 331 | stack.append(i) 332 | item = node[i] 333 | 334 | if isinstance(item, self.Label): 335 | if item.name in self.labels: 336 | raise IFPError( 337 | "Sequence Labels must be uniquely named within the Sequence. " 338 | f'Label "{item.name}" at location {stack} was previously defined ' 339 | "for this Sequence." 340 | ) 341 | self.labels[item.name] = list(stack) 342 | stack.pop() 343 | continue 344 | 345 | if type(item) is str or isinstance(item, self.ControlItem): 346 | stack.pop() 347 | continue 348 | 349 | if callable(item): 350 | sig = signature(item) 351 | if len([p for p in sig.parameters]) != 1: 352 | raise IFPError( 353 | f"{item} found in Sequence. " 354 | "Callables used as Sequence items must accept the Sequence instance " 355 | "as the only argument." 356 | f"\nLocation: {stack}" 357 | ) 358 | stack.pop() 359 | continue 360 | try: 361 | for key, sub_node in item.items(): 362 | if type(key) is not str: 363 | raise IFPError( 364 | "Only strings can be used as option names (dict keys) in Sequences. " 365 | f"Found {key} ({type(key)})\nLocation: {stack}" 366 | ) 367 | stack.append(key) 368 | self._parse_template_node(sub_node, stack=stack) 369 | stack.pop() 370 | except AttributeError: 371 | raise IFPError( 372 | f"Expected Sequence item (string, function, dict or Sequence " 373 | f"ControlItem); found {item}" 374 | f"\nLocation: {stack}" 375 | ) 376 | stack.pop() 377 | -------------------------------------------------------------------------------- /intficpy/serializer.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pickle 3 | import types 4 | 5 | from .ifp_object import IFPObject 6 | from .exceptions import DeserializationError, Unserializable 7 | 8 | ############################################################## 9 | # SERIALIZER.PY - the save/load system for IntFicPy 10 | # Defines the SaveState class, with methods for saving and loading games 11 | ############################################################## 12 | # TODO: do not load bad save files. create a back up of game state before attempting to load, and restore in the event of an error AT ANY POINT during loading 13 | 14 | 15 | class SaveGame: 16 | def __init__(self, game, filename): 17 | self.game = game 18 | self.filename = self.create_save_file_path(filename) 19 | self.data = { 20 | "ifp_objects": self.save_ifp_objects(), 21 | "locations": self.save_locations(), 22 | "active_sequence": self.serialize_attribute( 23 | game.parser.previous_command.sequence 24 | ), 25 | } 26 | self.file = open(self.filename, "wb+") 27 | pickle.dump(self.data, self.file, 0) 28 | self.file.close() 29 | 30 | def save_ifp_objects(self): 31 | out = {} 32 | for ix, obj in self.game.ifp_objects.items(): 33 | out[ix] = self.serialize_ifp_object(obj) 34 | return out 35 | 36 | def serialize_ifp_object(self, obj): 37 | out = {} 38 | 39 | for attr, value in obj.__dict__.items(): 40 | if attr in ["contains", "sub_contains"]: 41 | # contains is handled in the location section 42 | continue 43 | 44 | try: 45 | out[attr] = self.serialize_attribute(value) 46 | except Unserializable: 47 | pass 48 | 49 | return out 50 | 51 | def serialize_attribute(self, value): 52 | """ 53 | Recursively serialize an attribute 54 | Raises Unserializable in the event of unserializable attribute 55 | """ 56 | if isinstance(value, IFPObject): 57 | return f"{value.ix}" 58 | 59 | if isinstance(value, str): 60 | return value 61 | 62 | try: 63 | out = {} 64 | for sub_attr, sub_value in value.items(): 65 | out[sub_attr] = self.serialize_attribute(sub_value) 66 | return out 67 | 68 | except AttributeError: 69 | pass 70 | 71 | try: 72 | out = [] 73 | for sub_value in value: 74 | out.append(self.serialize_attribute(sub_value)) 75 | return out 76 | 77 | except TypeError: 78 | pass 79 | 80 | if ( 81 | hasattr(value, "__dict__") 82 | or isinstance(value, types.FunctionType) 83 | or isinstance(value, types.MethodType) 84 | ): 85 | raise Unserializable("Cannot serialize attribute.") 86 | 87 | return value 88 | 89 | def save_locations(self): 90 | top_level_locations = [ 91 | obj 92 | for key, obj in self.game.ifp_objects.items() 93 | if obj.is_top_level_location 94 | ] 95 | out = {} 96 | for obj in top_level_locations: 97 | out[obj.ix] = self.serialize_contains(obj) 98 | return out 99 | 100 | def serialize_contains(self, obj): 101 | serialized_contents = {} 102 | 103 | for key, sublist in obj.contains.items(): 104 | serialized_contents[key] = [] 105 | for item in sublist: 106 | serialized_contents[key].append(self.serialize_contains(item)) 107 | 108 | return {"ix": obj.ix, "contains": serialized_contents, "placed": False} 109 | 110 | def create_save_file_path(self, filename): 111 | # check if we have a full path 112 | directory = os.path.join(*os.path.split(filename)[:-1]) 113 | 114 | if not os.path.isdir(directory): 115 | filename = os.path.join( 116 | os.path.dirname(os.path.realpath(__file__)), filename 117 | ) 118 | 119 | if "." in filename and not filename[-4:] == ".sav": 120 | filename = filename[: filename.index(".")] + ".sav" 121 | 122 | elif not filename[-4:] == ".sav": 123 | filename += ".sav" 124 | 125 | return filename 126 | 127 | 128 | class LoadGame: 129 | single_object_keys = ["active_sequence"] 130 | allowed_keys = ["ifp_objects", "locations", "active_sequence"] 131 | 132 | def __init__(self, game, filename): 133 | self.game = game 134 | self.filename = filename 135 | self.file = open(self.filename, "rb") 136 | self.data = pickle.load(self.file) 137 | self.file.close() 138 | 139 | def is_valid(self): 140 | """ 141 | Try deserializing the data. 142 | On success, return True, and set load_file.validated_data 143 | On failure, delete the temporary objects, and return False 144 | """ 145 | for key, subdict in self.data.items(): 146 | if not key in self.allowed_keys: 147 | return False 148 | if key in self.single_object_keys: 149 | try: 150 | self.deserialize_attribute(value) 151 | except DeserializationError: 152 | return False 153 | continue 154 | 155 | for ix, obj in subdict.items(): 156 | if not ix in self.game.ifp_objects: 157 | return False 158 | 159 | for attr, value in obj.items(): 160 | try: 161 | self.deserialize_attribute(value) 162 | except DeserializationError: 163 | return False 164 | 165 | self.validated_data = self.data 166 | return True 167 | 168 | def load(self): 169 | if not hasattr(self, "validated_data"): 170 | raise DeserializationError("Call is_valid before loading game.") 171 | self.load_ifp_objects() 172 | self.load_locations() 173 | self.game.parser.previous_command.sequence = self.deserialize_attribute( 174 | self.validated_data["active_sequence"] 175 | ) 176 | return True 177 | 178 | def load_ifp_objects(self): 179 | if not hasattr(self, "validated_data"): 180 | raise DeserializationError("Call is_valid before loading game.") 181 | 182 | for ix, obj_data in self.validated_data["ifp_objects"].items(): 183 | obj = self.game.ifp_objects[ix] 184 | 185 | for attr, value in obj_data.items(): 186 | setattr(obj, attr, self.deserialize_attribute(value)) 187 | 188 | def load_locations(self): 189 | self.placed_things = [] 190 | 191 | if not hasattr(self, "validated_data"): 192 | raise DeserializationError("Call is_valid before loading game.") 193 | 194 | for ix, obj_data in self.validated_data["locations"].items(): 195 | obj = self.game.ifp_objects[ix] 196 | self.empty_contains(obj) 197 | self.populate_contains(obj, obj_data["contains"]) 198 | 199 | del self.placed_things 200 | 201 | def empty_contains(self, obj): 202 | contains = [item for ix, sublist in obj.contains.items() for item in sublist] 203 | for item in contains: 204 | if obj.containsItem(item): 205 | self.empty_contains(item) 206 | obj.removeThing(item) 207 | 208 | def add_thing_by_ix(self, destination, ix): 209 | """ 210 | Adds a Thing to a location (Room/Thing) by index. Makes a copy if a Thing of 211 | the specified index has already been placed. 212 | destination is a PhysicalEntity subclass instance 213 | """ 214 | if ix in self.placed_things: 215 | item = self.game.ifp_objects[ix].copyThing() 216 | else: 217 | self.placed_things.append(ix) 218 | item = self.game.ifp_objects[ix] 219 | for word in item.synonyms: 220 | if not word in self.game.nouns: 221 | self.game.nouns[word] = [item] 222 | elif not item in self.game.nouns[word]: 223 | self.game.nouns[word].append(item) 224 | destination.addThing(item) 225 | return item 226 | 227 | def populate_contains(self, root_obj, dict_in): 228 | """ 229 | Uses a recursive depth first search to place all items in the correct location 230 | root_obj is the object to populate, dict_in is the dictionary to read from 231 | """ 232 | for key in dict_in: 233 | for obj_data in dict_in[key]: 234 | if not obj_data["placed"]: 235 | found_obj = self.add_thing_by_ix(root_obj, key) 236 | self.empty_contains(found_obj) 237 | obj_data["placed"] = True 238 | self.populate_contains(found_obj, obj_data["contains"]) 239 | 240 | def deserialize_attribute(self, value): 241 | """ 242 | Recursively deserialize an attribute 243 | Raises DeserializationError in the event of an attribute 244 | that cannot be deseriliazed 245 | """ 246 | if isinstance(value, str) and value[:5] == "": 247 | ix = value[5:] 248 | if not ix in self.game.ifp_objects: 249 | raise DeserializationError 250 | return self.game.ifp_objects[ix] 251 | 252 | elif isinstance(value, str): 253 | return value 254 | 255 | try: 256 | out = {} 257 | for sub_attr, sub_value in value.items(): 258 | out[sub_attr] = self.deserialize_attribute(sub_value) 259 | return out 260 | 261 | except AttributeError: 262 | pass 263 | 264 | try: 265 | out = [] 266 | for sub_value in value: 267 | out.append(self.deserialize_attribute(sub_value)) 268 | return out 269 | 270 | except TypeError: 271 | pass 272 | 273 | return value 274 | -------------------------------------------------------------------------------- /intficpy/tokenizer.py: -------------------------------------------------------------------------------- 1 | import string 2 | 3 | from .vocab import english 4 | 5 | ############################################################## 6 | # TOKENIZER.PY - tokenizing and cleaning functions for IntFicPy 7 | ############################################################## 8 | 9 | 10 | def cleanInput(input_string, record=True): 11 | """ 12 | Used on player commands to remove punctuation and convert to lowercase 13 | Takes the raw user input (string) 14 | Returns a string 15 | """ 16 | input_string = input_string.lower() 17 | exclude = set(string.punctuation) 18 | return "".join(ch for ch in input_string if ch not in exclude) 19 | 20 | 21 | def tokenize(input_string): 22 | """Convert input to a list of tokens 23 | Takes a string as an argument, and returns a list of strings """ 24 | # tokenize input with spaces 25 | tokens = input_string.split() 26 | return tokens 27 | 28 | 29 | def removeArticles(tokens): 30 | for article in english.articles: 31 | while article in tokens: 32 | tokens.remove(article) 33 | return tokens 34 | -------------------------------------------------------------------------------- /intficpy/tools.py: -------------------------------------------------------------------------------- 1 | # TOOLS.PY 2 | # A collection of miscellaneous functions to simplify common tasks in IntFicPy 3 | 4 | from .thing_base import Thing 5 | from .actor import Actor 6 | from .travel import travel, room 7 | import intficpy.score as score 8 | 9 | 10 | def isSerializableClassInstance(obj): 11 | return ( 12 | isinstance(obj, Thing) 13 | or isinstance(obj, Actor) 14 | or isinstance(obj, score.Achievement) 15 | or isinstance(obj, score.Ending) 16 | or isinstance(obj, thing.Abstract) 17 | or isinstance(obj, actor.Topic) 18 | or isinstance(obj, travel.TravelConnector) 19 | or isinstance(obj, room.Room) 20 | or isinstance(obj, actor.SpecialTopic) 21 | or isinstance(obj, actor.SaleItem) 22 | or isinstance(obj, score.HintNode) 23 | or isinstance(obj, room.RoomGroup) 24 | or isinstance(obj, score.Hint) 25 | ) 26 | 27 | 28 | def isIFPClassInstance(obj): 29 | return ( 30 | isinstance(obj, Thing) 31 | or isinstance(obj, Actor) 32 | or isinstance(obj, score.Achievement) 33 | or isinstance(obj, score.Ending) 34 | or isinstance(obj, thing.Abstract) 35 | or isinstance(obj, actor.Topic) 36 | or isinstance(obj, travel.TravelConnector) 37 | or isinstance(obj, room.Room) 38 | or isinstance(obj, actor.SpecialTopic) 39 | or isinstance(obj, actor.SaleItem) 40 | or isinstance(obj, score.HintNode) 41 | or isinstance(obj, room.RoomGroup) 42 | or isinstance(obj, score.Hint) 43 | ) 44 | 45 | 46 | def lineDefinesNewIx(line): 47 | """Checks if a line of code in the game file defines an IFP object with a new index """ 48 | return ( 49 | " Thing(" in line 50 | or " Surface(" in line 51 | or " Container(" in line 52 | or " Clothing(" in line 53 | or " Abstract(" in line 54 | or " Key(" in line 55 | or " Lock(" in line 56 | or " UnderSpace(" in line 57 | or " LightSource(" in line 58 | or " Transparent(" in line 59 | or " Readable(" in line 60 | or " Book(" in line 61 | or " Pressable(" in line 62 | or " Liquid(" in line 63 | or " Room(" in line 64 | or " OutdoorRoom(" in line 65 | or " RoomGroup(" in line 66 | or " Achievement(" in line 67 | or " Ending(" in line 68 | or " TravelConnector(" in line 69 | or " DoorConnector(" in line 70 | or " LadderConnector(" in line 71 | or " StaircaseConnector(" in line 72 | or " Actor(" in line 73 | or " Player(" in line 74 | or " Topic(" in line 75 | or " SpecialTopic(" in line 76 | or "=Thing(" in line 77 | or "=Surface(" in line 78 | or "=Container(" in line 79 | or "=Clothing(" in line 80 | or "=Abstract(" in line 81 | or "=Key(" in line 82 | or "=Lock(" in line 83 | or "=UnderSpace(" in line 84 | or "=LightSource(" in line 85 | or "=Transparent(" in line 86 | or "=Readable(" in line 87 | or "=Book(" in line 88 | or "=Pressable(" in line 89 | or "=Liquid(" in line 90 | or "=Room(" in line 91 | or "=OutdoorRoom(" in line 92 | or "=RoomGroup(" in line 93 | or "=Achievement(" in line 94 | or "=Ending(" in line 95 | or "=TravelConnector(" in line 96 | or "=DoorConnector(" in line 97 | or "=LadderConnector(" in line 98 | or "=StaircaseConnector(" in line 99 | or "=Actor(" in line 100 | or "=Player(" in line 101 | or "=Topic(" in line 102 | or "=SpecialTopic(" in line 103 | or ".copyThingUniqueIx(" in line 104 | ) 105 | -------------------------------------------------------------------------------- /intficpy/vocab.py: -------------------------------------------------------------------------------- 1 | ############################################################## 2 | # VOCAB.PY - the standard vocabulary dictionaries for IntFicPy 3 | ############################################################## 4 | 5 | # vocab standard to the language 6 | class VocabObject: 7 | def __init__(self): 8 | self.prepositions = [] 9 | self.articles = [] 10 | self.keywords = [] 11 | self.no_space_before = [] 12 | self.yes = [] 13 | self.no = [] 14 | 15 | 16 | english = VocabObject() 17 | english.prepositions = [ 18 | "in", 19 | "out", 20 | "up", 21 | "down", 22 | "on", 23 | "under", 24 | "over", 25 | "through", 26 | "at", 27 | "across", 28 | "with", 29 | "off", 30 | "around", 31 | "to", 32 | "about", 33 | "from", 34 | "into", 35 | "using", 36 | ] 37 | english.articles = ["a", "an", "the"] 38 | english.keywords = ["all", "everything"] 39 | english.no_space_before = [ 40 | ",", 41 | ".", 42 | "?", 43 | "!", 44 | ":", 45 | ";", 46 | "'s", 47 | "'s", 48 | "'d", 49 | "'d", 50 | "'ll", 51 | "'ll", 52 | ] 53 | english.yes = ["yes", "y"] 54 | english.no = ["no", "n"] 55 | -------------------------------------------------------------------------------- /pre-commit: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RALWORKS/intficpy/3d0e40e1510d4ee07ae13f0cc4ac36926206cdbe/pre-commit -------------------------------------------------------------------------------- /scripts/rule_checker.py: -------------------------------------------------------------------------------- 1 | # script to check for unsafe IFP object definitions 2 | # because IFP saving/loading uses a dictionary of indeces generated at runtime, order must be preserved in object definitions 3 | # this script checks for IFP objects defined, or copied with a unique index, in loops, methods, or functions 4 | 5 | import sys 6 | 7 | cur_indent = 0 8 | next_indent = 0 9 | errors = 0 10 | warnings = 0 11 | cur_line = 0 12 | stack = [] 13 | file = input("enter path of file to analyze > ") 14 | file = open(file, "r") 15 | if not file: 16 | print("file not found") 17 | sys.exit(0) 18 | 19 | last_line = [] 20 | print("\n") 21 | for line in file: 22 | cur_line += 1 23 | next_indent = 0 24 | for char in line: 25 | if char.isspace(): 26 | next_indent = +1 27 | else: 28 | break 29 | if next_indent < cur_indent: 30 | stack.pop() 31 | elif next_indent > cur_indent: 32 | if last_line[0] == "def": 33 | stack.append("def") 34 | elif last_line[0] in ["if", "elif", "else:"]: 35 | stack.append("con") 36 | elif last_line[0] in ["while", "for"]: 37 | stack.append("loop") 38 | else: 39 | stack.append("x") 40 | cur_indent = next_indent 41 | last_line = line.split() 42 | if "con" in stack or "def" in stack: 43 | if ( 44 | " Thing(" in line 45 | or " Surface(" in line 46 | or " Container(" in line 47 | or " Clothing(" in line 48 | or " Abstract(" in line 49 | or " Key(" in line 50 | or " Lock(" in line 51 | or " UnderSpace(" in line 52 | or " LightSource(" in line 53 | or " Transparent(" in line 54 | or " Readable(" in line 55 | or " Book(" in line 56 | or " Pressable(" in line 57 | or " Liquid(" in line 58 | or " Room(" in line 59 | or " OutdoorRoom(" in line 60 | or " RoomGroup(" in line 61 | or " Achievement(" in line 62 | or " Ending(" in line 63 | or " TravelConnector(" in line 64 | or " DoorConnector(" in line 65 | or " LadderConnector(" in line 66 | or " StaircaseConnector(" in line 67 | or " Actor(" in line 68 | or " Player(" in line 69 | or " Topic(" in line 70 | or " SpecialTopic(" in line 71 | or "=Thing(" in line 72 | or "=Surface(" in line 73 | or "=Container(" in line 74 | or "=Clothing(" in line 75 | or "=Abstract(" in line 76 | or "=Key(" in line 77 | or "=Lock(" in line 78 | or "=UnderSpace(" in line 79 | or "=LightSource(" in line 80 | or "=Transparent(" in line 81 | or "=Readable(" in line 82 | or "=Book(" in line 83 | or "=Pressable(" in line 84 | or "=Liquid(" in line 85 | or "=Room(" in line 86 | or "=OutdoorRoom(" in line 87 | or "=RoomGroup(" in line 88 | or "=Achievement(" in line 89 | or "=Ending(" in line 90 | or "=TravelConnector(" in line 91 | or "=DoorConnector(" in line 92 | or "=LadderConnector(" in line 93 | or "=StaircaseConnector(" in line 94 | or "=Actor(" in line 95 | or "=Player(" in line 96 | or "=Topic(" in line 97 | or "=SpecialTopic(" in line 98 | or ".copyThingUniqueIx(" in line 99 | ): 100 | print( 101 | "ERROR: Unique IntFicPy object (new index) created in function, method, or conditional statement. This should be done at the top level only, to preserve order for saving/loading" 102 | ) 103 | print("line " + str(cur_line) + ":") 104 | print(line + "\n") 105 | errors += 1 106 | 107 | elif "loop" in stack: 108 | if ( 109 | " Thing(" in line 110 | or " Surface(" in line 111 | or " Container(" in line 112 | or " Clothing(" in line 113 | or " Abstract(" in line 114 | or " Key(" in line 115 | or " Lock(" in line 116 | or " UnderSpace(" in line 117 | or " LightSource(" in line 118 | or " Transparent(" in line 119 | or " Readable(" in line 120 | or " Book(" in line 121 | or " Pressable(" in line 122 | or " Liquid(" in line 123 | or " Room(" in line 124 | or " OutdoorRoom(" in line 125 | or " RoomGroup(" in line 126 | or " Achievement(" in line 127 | or " Ending(" in line 128 | or " TravelConnector(" in line 129 | or " DoorConnector(" in line 130 | or " LadderConnector(" in line 131 | or " StaircaseConnector(" in line 132 | or " Actor(" in line 133 | or " Player(" in line 134 | or " Topic(" in line 135 | or " SpecialTopic(" in line 136 | or "=Thing(" in line 137 | or "=Surface(" in line 138 | or "=Container(" in line 139 | or "=Clothing(" in line 140 | or "=Abstract(" in line 141 | or "=Key(" in line 142 | or "=Lock(" in line 143 | or "=UnderSpace(" in line 144 | or "=LightSource(" in line 145 | or "=Transparent(" in line 146 | or "=Readable(" in line 147 | or "=Book(" in line 148 | or "=Pressable(" in line 149 | or "=Liquid(" in line 150 | or "=Room(" in line 151 | or "=OutdoorRoom(" in line 152 | or "=RoomGroup(" in line 153 | or "=Achievement(" in line 154 | or "=Ending(" in line 155 | or "=TravelConnector(" in line 156 | or "=DoorConnector(" in line 157 | or "=LadderConnector(" in line 158 | or "=StaircaseConnector(" in line 159 | or "=Actor(" in line 160 | or "=Player(" in line 161 | or "=Topic(" in line 162 | or "=SpecialTopic(" in line 163 | or ".copyThingUniqueIx(" in line 164 | ): 165 | print( 166 | "WARNING: Unique IntFicPy object (new index) created in loop. This is safe only if the loop will always run the same number of times" 167 | ) 168 | print("line " + str(cur_line) + ":") 169 | print(line + "\n") 170 | warnings += 1 171 | 172 | print("Analysis complete.") 173 | print(str(errors) + " errors; " + str(warnings) + " warnings") 174 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from distutils.core import setup 4 | 5 | setup( 6 | name="IntFicPy", 7 | version="0.5", 8 | description="Python parser-based interactive fiction engine", 9 | author="JSMaika", 10 | author_email="r.a.lester121@gmail.com", 11 | packages=["intficpy"], 12 | install_requires=[], # external packages as dependencies 13 | ) 14 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RALWORKS/intficpy/3d0e40e1510d4ee07ae13f0cc4ac36926206cdbe/tests/__init__.py -------------------------------------------------------------------------------- /tests/helpers.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | import random 3 | 4 | from intficpy.actor import Player 5 | from intficpy.room import Room 6 | from intficpy.ifp_game import IFPGame 7 | 8 | 9 | class TestApp: 10 | def __init__(self): 11 | self.print_stack = [] 12 | 13 | def printEventText(self, event): 14 | for t in event.text: 15 | self.print_stack.append(t) 16 | 17 | 18 | class IFPTestCase(TestCase): 19 | def setUp(self): 20 | self.app = TestApp() 21 | self.game = IFPGame(self.app, main=__name__) 22 | self.me = Player(self.game) 23 | self.start_room = Room(self.game, "room", "desc") 24 | self.start_room.addThing(self.me) 25 | self.game.setPlayer(self.me) 26 | self.game.initGame() 27 | 28 | def _insert_dobj_into_phrase(self, phrase, dobj): 29 | ix = phrase.index("") 30 | phrase = phrase[:ix] + dobj + phrase[ix + 1 :] 31 | return phrase 32 | 33 | def _insert_iobj_into_phrase(self, phrase, iobj): 34 | ix = phrase.index("") 35 | phrase = phrase[:ix] + iobj + phrase[ix + 1 :] 36 | return phrase 37 | 38 | def _insert_objects_into_phrase(self, phrase, dobj, iobj): 39 | phrase = self._insert_dobj_into_phrase(phrase, dobj) 40 | phrase = self._insert_iobj_into_phrase(phrase, iobj) 41 | return phrase 42 | 43 | def _get_unique_noun(self): 44 | noun = str(random.getrandbits(128)) 45 | if noun in self.game.nouns: 46 | noun = self._get_unique_noun() 47 | return noun 48 | 49 | def assertItemIn(self, item, contains_dict, msg): 50 | self.assertIn(item.ix, contains_dict, f"Index not in dictionary: {msg}") 51 | self.assertIn( 52 | item, 53 | contains_dict[item.ix], 54 | f"Index in dictionary, but item not found under index: {msg}", 55 | ) 56 | 57 | def assertItemNotIn(self, item, contains_dict, msg): 58 | if item.ix in contains_dict: 59 | self.assertNotIn( 60 | item, 61 | contains_dict[item.ix], 62 | f"Item unexpectedly found in dictionary: {msg}", 63 | ) 64 | 65 | def assertItemExactlyOnceIn(self, item, contains_dict, msg): 66 | self.assertIn(item.ix, contains_dict, f"Index not in dictionary: {msg}") 67 | n = len(contains_dict[item.ix]) 68 | self.assertEqual( 69 | n, 1, f"Expected a single occurrence of item {item}, found {n}: {msg}" 70 | ) 71 | -------------------------------------------------------------------------------- /tests/test_desc.py: -------------------------------------------------------------------------------- 1 | from .helpers import IFPTestCase 2 | from intficpy.thing_base import Thing 3 | from intficpy.things import Surface, Container, Lock, UnderSpace 4 | 5 | 6 | class TestDesc(IFPTestCase): 7 | def test_desc_is_blank_if_description_is_blank(self): 8 | subject = Thing(self.game, self._get_unique_noun()) 9 | subject.description = "" 10 | self.assertEqual(subject.desc, "") 11 | 12 | def test_xdesc_is_blank_if_x_description_is_blank(self): 13 | subject = Thing(self.game, self._get_unique_noun()) 14 | subject.x_description = "" 15 | self.assertEqual(subject.xdesc, "") 16 | 17 | def test_room_desc_contains_description_of_described_item_in_room(self): 18 | subject = Thing(self.game, self._get_unique_noun()) 19 | subject.description = ( 20 | f"A very large, very strange {subject.verbose_name} lurks in the corner." 21 | ) 22 | self.start_room.addThing(subject) 23 | self.game.turnMain(f"l") 24 | msg = self.app.print_stack.pop() 25 | self.assertIn(subject.description, msg) 26 | 27 | def test_desc_contains_contents_if_desc_reveal(self): 28 | subject = Surface(self.game, self._get_unique_noun()) 29 | subject.desc_reveal = True 30 | content = Thing(self.game, self._get_unique_noun()) 31 | subject.addThing(content) 32 | self.start_room.addThing(subject) 33 | self.game.turnMain(f"l") 34 | msg = self.app.print_stack.pop() 35 | self.assertIn(content.verbose_name, msg) 36 | 37 | def test_xdesc_does_not_contain_contents_if_not_xdesc_reveal(self): 38 | subject = Surface(self.game, self._get_unique_noun()) 39 | subject.xdesc_reveal = False 40 | content = Thing(self.game, self._get_unique_noun()) 41 | subject.addThing(content) 42 | self.start_room.addThing(subject) 43 | self.game.turnMain(f"x {subject.verbose_name}") 44 | msg = self.app.print_stack.pop() 45 | self.assertNotIn(content.verbose_name, msg) 46 | 47 | def test_desc_does_not_contain_contents_if_lid_is_closed(self): 48 | subject = Container(self.game, self._get_unique_noun()) 49 | subject.giveLid() 50 | subject.is_open = False 51 | content = Thing(self.game, self._get_unique_noun()) 52 | subject.addThing(content) 53 | self.start_room.addThing(subject) 54 | self.game.turnMain(f"x {subject.verbose_name}") 55 | msg = self.app.print_stack.pop() 56 | self.assertNotIn(content.verbose_name, msg) 57 | 58 | def test_desc_contains_lid_state(self): 59 | subject = Container(self.game, self._get_unique_noun()) 60 | subject.giveLid() 61 | content = Thing(self.game, self._get_unique_noun()) 62 | subject.addThing(content) 63 | self.start_room.addThing(subject) 64 | subject.is_open = False 65 | self.game.turnMain(f"x {subject.verbose_name}") 66 | msg = self.app.print_stack.pop() 67 | self.assertIn("is closed", msg) 68 | 69 | def test_desc_contains_lock_state(self): 70 | subject = Container(self.game, self._get_unique_noun()) 71 | subject.giveLid() 72 | lock = Lock(self.game, False, None) 73 | subject.setLock(lock) 74 | self.game.turnMain(f"x {subject.verbose_name}") 75 | msg = self.app.print_stack.pop() 76 | self.assertNotIn("is locked", msg) 77 | 78 | def test_contains_list_does_not_contain_composite_child_items(self): 79 | subject = Container(self.game, self._get_unique_noun()) 80 | content = Thing(self.game, self._get_unique_noun()) 81 | child = Thing(self.game, "child") 82 | content.addComposite(child) 83 | subject.addThing(content) 84 | self.start_room.addThing(subject) 85 | self.game.turnMain(f"x {subject.verbose_name}") 86 | msg = self.app.print_stack.pop() 87 | self.assertNotIn(child.verbose_name, msg) 88 | 89 | def test_undescribed_underspace_not_included_in_composite_desc(self): 90 | subject = Thing(self.game, self._get_unique_noun()) 91 | child = UnderSpace(self.game, "child") 92 | subject.addComposite(child) 93 | self.start_room.addThing(subject) 94 | 95 | self.assertNotIn(child.verbose_name, subject.composite_desc) 96 | 97 | self.game.turnMain(f"l {subject.verbose_name}") 98 | msg = self.app.print_stack.pop() 99 | self.assertNotIn(child.verbose_name, msg) 100 | 101 | 102 | class TestPlural(IFPTestCase): 103 | def test_plural_of_plural_returns_verbose_name(self): 104 | subject = Thing(self.game, "beads") 105 | subject.is_plural = True 106 | self.assertEqual(subject.verbose_name, subject.plural) 107 | 108 | def test_desc_of_plural_conjugates_correctly(self): 109 | subject = Thing(self.game, "beads") 110 | self.assertNotIn("are", subject.desc) 111 | subject.is_plural = True 112 | self.assertIn("are", subject.desc) 113 | 114 | def test_plural_uses_special_plural(self): 115 | subject = Thing(self.game, "fungus") 116 | subject.special_plural = "fungi" 117 | self.assertEqual(subject.plural, subject.special_plural) 118 | 119 | 120 | class TestVerboseName(IFPTestCase): 121 | def test_verbose_name_contains_all_adjectives_in_order_if_not_overridden(self): 122 | subject = Thing(self.game, "flower") 123 | ADJECTIVES = ["grandma's", "big", "bright", "yellow"] 124 | subject.setAdjectives(ADJECTIVES) 125 | expected_name = " ".join(ADJECTIVES + [subject.name]) 126 | self.assertEqual(expected_name, subject.verbose_name) 127 | 128 | def test_verbose_name_is_full_name_if_overridden(self): 129 | subject = Thing(self.game, "flower") 130 | ADJECTIVES = ["grandma's", "big", "bright", "yellow"] 131 | subject.setAdjectives(ADJECTIVES) 132 | NAME = "sasquatch" 133 | subject.full_name = NAME 134 | self.assertEqual(NAME, subject.verbose_name) 135 | 136 | def test_legacy_underscore_verbose_name_attr_alias(self): 137 | subject = Thing(self.game, "flower") 138 | self.assertFalse(subject.full_name) 139 | FULL_NAME = "daisy flower" 140 | subject._verbose_name = FULL_NAME 141 | self.assertEqual(subject._verbose_name, FULL_NAME) 142 | self.assertEqual(subject.full_name, FULL_NAME) 143 | -------------------------------------------------------------------------------- /tests/test_event.py: -------------------------------------------------------------------------------- 1 | from .helpers import IFPTestCase 2 | 3 | 4 | class TestNestedEvents(IFPTestCase): 5 | def test_nested_events_text_prints_in_order(self): 6 | FIRST = "first" 7 | SECOND = "second" 8 | THIRD = "third" 9 | 10 | self.game.addSubEvent("turn", "implicit") 11 | self.game.addText(THIRD) 12 | self.game.addTextToEvent("implicit", FIRST) 13 | self.game.addTextToEvent("implicit", SECOND) 14 | 15 | self.game.runTurnEvents() 16 | self.assertEqual(self.app.print_stack.pop(), THIRD) 17 | self.assertEqual(self.app.print_stack.pop(), SECOND) 18 | self.assertEqual(self.app.print_stack.pop(), FIRST) 19 | 20 | def test_text_property(self): 21 | self.assertFalse(self.game.next_events["turn"].text) 22 | 23 | self.game.addSubEvent("turn", "implicit") 24 | self.assertFalse(self.game.next_events["turn"].text) 25 | 26 | self.game.addTextToEvent("implicit", "text") 27 | self.assertEqual(len(self.game.next_events["turn"].text), 1) 28 | 29 | self.game.addText("more text") 30 | self.assertEqual(len(self.game.next_events["turn"].text), 2) 31 | -------------------------------------------------------------------------------- /tests/test_game.py: -------------------------------------------------------------------------------- 1 | from .helpers import IFPTestCase 2 | 3 | 4 | class TestAddText(IFPTestCase): 5 | def test_add_text_with_no_turn_raises(self): 6 | del self.game.next_events["turn"] 7 | with self.assertRaises(KeyError): 8 | self.game.addText("Geraldine smiles.") 9 | 10 | def test_add_text(self): 11 | text = "Geraldine smiles." 12 | self.game.addText(text) 13 | self.assertIn(text, self.game.next_events["turn"].text) 14 | 15 | self.game.turnMain("l") 16 | 17 | self.assertIn(text, self.app.print_stack) 18 | -------------------------------------------------------------------------------- /tests/test_info_commands.py: -------------------------------------------------------------------------------- 1 | from .helpers import IFPTestCase 2 | 3 | 4 | class TestInfoCommands(IFPTestCase): 5 | def setUp(self): 6 | super().setUp() 7 | self.game.aboutGame.title = self._get_unique_noun() 8 | self.game.aboutGame.author = self._get_unique_noun() 9 | self.game.aboutGame.betaTesterCredit = self._get_unique_noun() 10 | self.game.aboutGame.desc = self._get_unique_noun() 11 | self.game.aboutGame.game_instructions = self._get_unique_noun() 12 | 13 | def test_about_prints_about_components(self): 14 | self.game.turnMain("about") 15 | 16 | about_components = { 17 | "title": f"{self.game.aboutGame.title}", 18 | "author": f"Created by {self.game.aboutGame.author}", 19 | "betaTesterCredit": self.game.aboutGame.betaTesterCredit, 20 | "desc": self.game.aboutGame.desc, 21 | } 22 | for key in about_components: 23 | with self.subTest(component=key): 24 | self.assertIn(about_components[key], self.app.print_stack) 25 | 26 | def test_intructions_prints_instructions_components(self): 27 | self.game.turnMain("instructions") 28 | 29 | instructions_components = { 30 | "basic_instructions": self.game.aboutGame.basic_instructions, 31 | "game_instructions": self.game.aboutGame.game_instructions, 32 | } 33 | for key in instructions_components: 34 | with self.subTest(component=key): 35 | self.assertIn(instructions_components[key], self.app.print_stack) 36 | -------------------------------------------------------------------------------- /tests/test_score.py: -------------------------------------------------------------------------------- 1 | from .helpers import IFPTestCase 2 | 3 | from intficpy.score import Achievement, Ending, Hint, HintNode 4 | from intficpy.things import Thing 5 | 6 | 7 | class TestAchevement(IFPTestCase): 8 | def test_award_achievement(self): 9 | ach = Achievement(self.game, 3, "getting the disc") 10 | item = Thing(self.game, "disc") 11 | item.moveTo(self.start_room) 12 | item.getVerbDobj = lambda x: ach.award(self.game) 13 | 14 | self.assertEqual(self.game.score.total, 0) 15 | 16 | self.game.turnMain("take disc") 17 | 18 | self.assertEqual(self.game.score.total, 3) 19 | self.game.turnMain("score") 20 | self.game.turnMain("fullscore") 21 | 22 | 23 | class TestEnding(IFPTestCase): 24 | def test_end_game(self): 25 | end = Ending(self.game, True, "Won!", "You won!") 26 | 27 | item = Thing(self.game, "disc") 28 | item.moveTo(self.start_room) 29 | item.getVerbDobj = lambda x: end.endGame(self.game) 30 | 31 | self.assertFalse(self.game.ended) 32 | 33 | self.game.turnMain("take disc") 34 | 35 | self.assertTrue(self.game.ended) 36 | 37 | self.game.turnMain("drop disc") 38 | 39 | self.assertIn("The game has ended", self.app.print_stack.pop()) 40 | 41 | self.game.turnMain("score") 42 | self.assertNotIn("The game has ended", self.app.print_stack.pop()) 43 | 44 | self.game.turnMain("fullscore") 45 | self.assertNotIn("The game has ended", self.app.print_stack.pop()) 46 | 47 | self.game.turnMain("full score") 48 | self.assertNotIn("The game has ended", self.app.print_stack.pop()) 49 | 50 | self.game.turnMain("about") 51 | self.assertNotIn("The game has ended", self.app.print_stack.pop()) 52 | 53 | 54 | class TestHints(IFPTestCase): 55 | def setUp(self): 56 | super().setUp() 57 | 58 | self.ach = Achievement(self.game, 3, "escaping the room") 59 | 60 | hint1 = Hint(self.game, "Hmm. How can I get out of here?", self.ach, 1) 61 | hint2 = Hint(self.game, "Exit through the door", self.ach, 1) 62 | self.node = HintNode(self.game, [hint1, hint2]) 63 | 64 | def test_get_hint(self): 65 | self.game.hints.setNode(self.game, self.node) 66 | 67 | ACH_REWARD = self.ach.points 68 | 69 | self.game.turnMain("hint") 70 | 71 | self.assertEqual(self.ach.points, ACH_REWARD - 1) 72 | 73 | self.game.turnMain("hint") 74 | 75 | self.assertEqual(self.ach.points, ACH_REWARD - 2) 76 | 77 | self.game.hints.closeNode(self.game, self.node) 78 | 79 | def test_pending_hint(self): 80 | self.game.hints.addPending(self.game, self.node) 81 | 82 | self.game.turnMain("hint") 83 | 84 | self.assertIn("no hint", self.app.print_stack.pop()) 85 | self.game.turnMain("hint") 86 | self.assertIn("Exit through", self.app.print_stack[-2]) 87 | 88 | def test_required_incomplete(self): 89 | hint3 = Hint(self.game, "Eagles in the roof") 90 | node2 = HintNode(self.game, [hint3]) 91 | node2.open_require_nodes_incomplete = [self.node] 92 | 93 | self.node.complete = True 94 | self.game.hints.addPending(self.game, node2) 95 | 96 | self.assertIn(node2, self.game.hints.pending) 97 | 98 | self.game.turnMain("hint") 99 | self.assertIn("no hint", self.app.print_stack.pop()) 100 | 101 | self.assertNotIn(node2, self.game.hints.pending) 102 | -------------------------------------------------------------------------------- /tests/test_sequence.py: -------------------------------------------------------------------------------- 1 | from intficpy.sequence import Sequence 2 | from intficpy.exceptions import IFPError 3 | 4 | from .helpers import IFPTestCase 5 | 6 | 7 | class TestSequence(IFPTestCase): 8 | def test_sequence_lifecycle(self): 9 | sequence = Sequence( 10 | self.game, ["This is the start", {"the only option": ["the outcome"]}] 11 | ) 12 | sequence.a_wonderful_strange = None 13 | 14 | def ended(): 15 | sequence.a_wonderful_strange = True 16 | 17 | sequence.on_complete = ended 18 | 19 | self.assertIsNone(self.game.parser.previous_command.sequence) 20 | sequence.start() 21 | self.assertIs(self.game.parser.command.sequence, sequence) 22 | self.game.runTurnEvents() 23 | self.assertIn(sequence.template[0], self.app.print_stack) 24 | 25 | self.game.turnMain("1") 26 | self.assertIs(self.game.parser.command.sequence, sequence) 27 | 28 | self.assertIn(sequence.template[1]["the only option"][0], self.app.print_stack) 29 | self.assertTrue(sequence.a_wonderful_strange) 30 | 31 | self.game.turnMain("l") 32 | self.assertIsNone(self.game.parser.command.sequence) 33 | 34 | def test_select_sequence_option_by_keywords(self): 35 | sequence = Sequence( 36 | self.game, [{"here it is": ["we shall"], "not here": ["no way"]}] 37 | ) 38 | sequence.start() 39 | self.game.runTurnEvents() 40 | 41 | key = sequence.options[0] 42 | 43 | self.game.turnMain(key) 44 | self.assertIn(sequence.template[0][key][0], self.app.print_stack) 45 | 46 | def test_no_matching_suggestion(self): 47 | sequence = Sequence( 48 | self.game, [{"here it is": ["we shall"], "not here": ["no way"]}] 49 | ) 50 | sequence.start() 51 | self.game.runTurnEvents() 52 | 53 | self.game.turnMain("The invisible man turns the invisible key") 54 | 55 | self.assertIn("here it is", self.app.print_stack.pop()) 56 | self.assertIn("not enough information", self.app.print_stack.pop()) 57 | 58 | def test_out_of_bound_option_index(self): 59 | sequence = Sequence( 60 | self.game, [{"here it is": ["we shall"], "not here": ["no way"]}] 61 | ) 62 | sequence.start() 63 | self.game.runTurnEvents() 64 | 65 | ix = 333 66 | self.assertGreater(ix, len(sequence.options)) 67 | 68 | self.game.turnMain(str(ix)) 69 | 70 | self.assertIn("here it is", self.app.print_stack.pop()) 71 | self.assertIn("not enough information", self.app.print_stack.pop()) 72 | 73 | def test_accept_selection_with_single_word_non_index(self): 74 | sequence = Sequence( 75 | self.game, [{"here it is": ["we shall"], "not here": ["no way"]}] 76 | ) 77 | sequence.start() 78 | self.game.runTurnEvents() 79 | 80 | self.game.turnMain("not") 81 | 82 | self.assertIn(sequence.template[0]["not here"][0], self.app.print_stack.pop()) 83 | 84 | def test_callable_as_sequence_item_prints_return_value_if_string(self): 85 | def locusts_swarm(seq): 86 | return "A swarm of locusts descends upon the land." 87 | 88 | sequence = Sequence(self.game, [locusts_swarm]) 89 | 90 | sequence.start() 91 | self.game.runTurnEvents() 92 | 93 | self.assertIn(locusts_swarm(sequence), self.app.print_stack) 94 | 95 | def test_callable_as_sequence_runs_and_does_not_print_if_return_value_not_string( 96 | self, 97 | ): 98 | self.game.locusts_evaluated = False 99 | 100 | def locusts_swarm(seq): 101 | self.game.locusts_evaluated = True 102 | return 17 103 | 104 | sequence = Sequence(self.game, [locusts_swarm]) 105 | 106 | sequence.start() 107 | self.game.runTurnEvents() 108 | 109 | self.assertNotIn(locusts_swarm(sequence), self.app.print_stack) 110 | self.assertNotIn(str(locusts_swarm(sequence)), self.app.print_stack) 111 | self.assertTrue(self.game.locusts_evaluated) 112 | 113 | def test_sequence_data_replacements(self): 114 | MC_NAME = "edmund" 115 | self.game.an_extra_something = "swamp" 116 | sequence = Sequence( 117 | self.game, 118 | ["{name}, here is a {game.an_extra_something}.",], 119 | data={"name": MC_NAME}, 120 | ) 121 | sequence.start() 122 | self.game.runTurnEvents() 123 | self.assertIn( 124 | sequence.template[0].format(name=MC_NAME, game=self.game), 125 | self.app.print_stack, 126 | ) 127 | 128 | def test_chaining_sequences(self): 129 | ITEM1 = "Hello" 130 | ITEM2 = "Again hello" 131 | sequence1 = Sequence(self.game, [ITEM1],) 132 | sequence2 = Sequence(self.game, [ITEM2],) 133 | sequence1.next_sequence = sequence2 134 | sequence1.start() 135 | self.game.runTurnEvents() 136 | self.assertIn(ITEM1, self.app.print_stack) 137 | self.assertIn(ITEM2, self.app.print_stack) 138 | 139 | def test_manual_pause(self): 140 | START_ITEM = "Hello." 141 | SKIPPED_ITEM = "NEVER!" 142 | 143 | sequence = Sequence(self.game, [START_ITEM, Sequence.Pause(), SKIPPED_ITEM,]) 144 | sequence.start() 145 | self.game.runTurnEvents() 146 | self.assertIn(START_ITEM, self.app.print_stack) 147 | self.assertNotIn(SKIPPED_ITEM, self.app.print_stack) 148 | 149 | 150 | class TestSequenceJump(IFPTestCase): 151 | def test_can_jump_by_label(self): 152 | START_ITEM = "Hello." 153 | SKIPPED_ITEM = "NEVER!" 154 | END_ITEM = "Goodbye." 155 | L = "sidestep" 156 | 157 | sequence = Sequence( 158 | self.game, 159 | [START_ITEM, Sequence.Jump(L), SKIPPED_ITEM, Sequence.Label(L), END_ITEM,], 160 | ) 161 | sequence.start() 162 | self.game.runTurnEvents() 163 | self.assertIn(START_ITEM, self.app.print_stack) 164 | self.assertIn(END_ITEM, self.app.print_stack) 165 | self.assertNotIn(SKIPPED_ITEM, self.app.print_stack) 166 | 167 | def test_can_jump_by_index(self): 168 | START_ITEM = "Hello." 169 | SKIPPED_ITEM = "NEVER!" 170 | END_ITEM = "Goodbye." 171 | 172 | sequence = Sequence( 173 | self.game, [START_ITEM, Sequence.Jump([2]), SKIPPED_ITEM, END_ITEM,], 174 | ) 175 | sequence.start() 176 | self.game.runTurnEvents() 177 | self.assertIn(START_ITEM, self.app.print_stack) 178 | self.assertIn(END_ITEM, self.app.print_stack) 179 | self.assertNotIn(SKIPPED_ITEM, self.app.print_stack) 180 | 181 | 182 | class TestSequenceNavigator(IFPTestCase): 183 | def test_can_navigate_by_label(self): 184 | START_ITEM = "Hello." 185 | SKIPPED_ITEM = "NEVER!" 186 | END_ITEM = "Goodbye." 187 | L = "sidestep" 188 | 189 | sequence = Sequence( 190 | self.game, 191 | [ 192 | START_ITEM, 193 | Sequence.Navigator(lambda s: L), 194 | SKIPPED_ITEM, 195 | Sequence.Label(L), 196 | END_ITEM, 197 | ], 198 | ) 199 | sequence.start() 200 | self.game.runTurnEvents() 201 | self.assertIn(START_ITEM, self.app.print_stack) 202 | self.assertIn(END_ITEM, self.app.print_stack) 203 | self.assertNotIn(SKIPPED_ITEM, self.app.print_stack) 204 | 205 | def test_can_navigate_by_index(self): 206 | START_ITEM = "Hello." 207 | SKIPPED_ITEM = "NEVER!" 208 | END_ITEM = "Goodbye." 209 | 210 | sequence = Sequence( 211 | self.game, 212 | [START_ITEM, Sequence.Navigator(lambda s: [2]), SKIPPED_ITEM, END_ITEM,], 213 | ) 214 | sequence.start() 215 | self.game.runTurnEvents() 216 | self.assertIn(START_ITEM, self.app.print_stack) 217 | self.assertIn(END_ITEM, self.app.print_stack) 218 | self.assertNotIn(SKIPPED_ITEM, self.app.print_stack) 219 | 220 | 221 | class TextSaveData(IFPTestCase): 222 | def test_save_data_control_item(self): 223 | MC_NAME = "edmund" 224 | self.game.an_extra_something = "swamp" 225 | sequence = Sequence( 226 | self.game, [Sequence.SaveData("name", MC_NAME)], data={"name": None} 227 | ) 228 | self.assertFalse(sequence.data["name"]) 229 | sequence.start() 230 | self.game.runTurnEvents() 231 | self.assertEqual(sequence.data["name"], MC_NAME) 232 | 233 | 234 | class TestSequencePrompt(IFPTestCase): 235 | def test_can_respond_to_prompt_and_retrieve_data(self): 236 | MC_NAME = "edmund" 237 | LABEL = "Your name" 238 | DATA_KEY = "mc_name" 239 | QUESTION = "What's your name?" 240 | sequence = Sequence( 241 | self.game, 242 | [ 243 | "My name's Izzy. What's yours?", 244 | Sequence.Prompt(DATA_KEY, LABEL, QUESTION), 245 | "Good to meet you.", 246 | ], 247 | ) 248 | sequence.start() 249 | self.game.runTurnEvents() 250 | self.assertIn(sequence.template[0], self.app.print_stack) 251 | self.game.turnMain(MC_NAME) 252 | self.assertIn(f"{LABEL}: {MC_NAME}? (y/n)", self.app.print_stack) 253 | self.game.turnMain("y") 254 | self.assertEqual(sequence.data[DATA_KEY], MC_NAME) 255 | self.assertIn(sequence.template[2], self.app.print_stack) 256 | 257 | def test_can_make_correction_before_submitting(self): 258 | TYPO_NAME = "edumd" 259 | MC_NAME = "edmund" 260 | LABEL = "Your name" 261 | DATA_KEY = "mc_name" 262 | QUESTION = "What's your name?" 263 | sequence = Sequence( 264 | self.game, 265 | [ 266 | "My name's Izzy. What's yours?", 267 | Sequence.Prompt(DATA_KEY, LABEL, QUESTION), 268 | "Good to meet you.", 269 | ], 270 | ) 271 | sequence.start() 272 | self.game.runTurnEvents() 273 | self.assertIn(sequence.template[0], self.app.print_stack) 274 | self.game.turnMain(TYPO_NAME) 275 | self.assertIn(f"{LABEL}: {TYPO_NAME}? (y/n)", self.app.print_stack) 276 | self.game.turnMain("n") 277 | self.assertIn(QUESTION, self.app.print_stack) 278 | self.game.turnMain(MC_NAME) 279 | self.game.turnMain("y") 280 | 281 | self.assertEqual(sequence.data[DATA_KEY], MC_NAME) 282 | self.assertIn(sequence.template[2], self.app.print_stack) 283 | 284 | 285 | class TestValidateSequenceTemplate(IFPTestCase): 286 | def test_invalid_top_level_template_node(self): 287 | with self.assertRaises(IFPError): 288 | Sequence(self.game, 8) 289 | 290 | def test_invalid_inner_template_node(self): 291 | with self.assertRaises(IFPError): 292 | Sequence(self.game, [{"hello": "string not list"}]) 293 | 294 | def test_invalid_sequence_item_type(self): 295 | with self.assertRaises(IFPError): 296 | Sequence(self.game, [8]) 297 | 298 | def test_dict_key_for_option_name_must_be_string(self): 299 | with self.assertRaises(IFPError): 300 | Sequence(self.game, [{6: ["item"]}]) 301 | 302 | def test_callable_accepting_too_many_arguments_is_invalid_item(self): 303 | def heron_event(seq, n_herons): 304 | return f"There are {n_herons} herons." 305 | 306 | with self.assertRaises(IFPError): 307 | Sequence(self.game, [heron_event]) 308 | 309 | def test_callable_accepting_no_arguments_is_invalid_item(self): 310 | def heron_event(): 311 | return f"There are 2 herons." 312 | 313 | with self.assertRaises(IFPError): 314 | Sequence(self.game, [heron_event]) 315 | 316 | def test_label_name_cannot_be_used_twice(self): 317 | L = "label" 318 | with self.assertRaises(IFPError): 319 | Sequence(self.game, [Sequence.Label(L), Sequence.Label(L)]) 320 | -------------------------------------------------------------------------------- /tests/test_serializer.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pickle 3 | import uuid 4 | 5 | from intficpy.serializer import SaveGame, LoadGame 6 | from intficpy.daemons import Daemon 7 | from intficpy.thing_base import Thing 8 | from intficpy.things import Surface, Container 9 | 10 | from .helpers import IFPTestCase 11 | 12 | 13 | class TestSaveLoadOneRoomWithPlayer(IFPTestCase): 14 | def setUp(self): 15 | super().setUp() 16 | FILENAME = f"_ifp_tests_saveload__{uuid.uuid4()}.sav" 17 | 18 | path = os.path.dirname(os.path.realpath(__file__)) 19 | self.path = os.path.join(path, FILENAME) 20 | 21 | def test_save_file_size_does_not_grow(self): 22 | size = [] 23 | locs_bytes = [] 24 | locs_keys = [] 25 | ifp_obj_bytes = [] 26 | ifp_obj_keys = [] 27 | 28 | initial_obj = None 29 | 30 | for i in range(0, 5): 31 | SaveGame(self.game, self.path) 32 | size.append(os.path.getsize(self.path)) 33 | l = LoadGame(self.game, self.path) 34 | self.assertTrue(l.is_valid()) 35 | 36 | locs_bytes.append(len(pickle.dumps(l.validated_data["locations"]))) 37 | ifp_obj_bytes.append(len(pickle.dumps(l.validated_data["ifp_objects"]))) 38 | 39 | locs_keys.append(len(l.validated_data["locations"])) 40 | ifp_obj_keys.append(len(l.validated_data["ifp_objects"])) 41 | 42 | if i == 0: 43 | initial_obj = l.validated_data 44 | elif i == 4: 45 | latest_obj = l.validated_data 46 | 47 | l.load() 48 | 49 | initial_obj_sizes = {} 50 | for key, value in initial_obj["ifp_objects"].items(): 51 | initial_obj_sizes[key] = len(pickle.dumps(value)) 52 | 53 | initial_loc_sizes = {} 54 | for key, value in initial_obj["locations"].items(): 55 | initial_loc_sizes[key] = len(pickle.dumps(value)) 56 | 57 | final_obj_sizes = {} 58 | for key, value in latest_obj["ifp_objects"].items(): 59 | final_obj_sizes[key] = len(pickle.dumps(value)) 60 | 61 | final_loc_sizes = {} 62 | for key, value in latest_obj["locations"].items(): 63 | final_loc_sizes[key] = len(pickle.dumps(value)) 64 | 65 | obj_deltas = {} 66 | for key, value in final_obj_sizes.items(): 67 | if value == initial_obj_sizes[key]: 68 | continue 69 | obj_deltas[abs(value - initial_loc_sizes[key])] = ( 70 | initial_obj["ifp_objects"][key], 71 | latest_obj["ifp_objects"][key], 72 | ) 73 | 74 | if obj_deltas: 75 | max_obj_delta = max(obj_deltas.keys()) 76 | most_changed_obj = obj_deltas[max_obj_delta] 77 | else: 78 | max_obj_delta = 0 79 | most_changed_obj = (None, None) 80 | 81 | loc_deltas = {} 82 | for key, value in final_loc_sizes.items(): 83 | if value == initial_loc_sizes[key]: 84 | continue 85 | loc_deltas[abs(value - initial_loc_sizes[key])] = ( 86 | initial_obj["locations"][key], 87 | latest_obj["locations"][key], 88 | ) 89 | 90 | if loc_deltas: 91 | max_loc_delta = max(loc_deltas.keys()) 92 | most_changed_loc = loc_deltas[max_loc_delta] 93 | else: 94 | max_loc_delta = 0 95 | most_changed_loc = (None, None) 96 | 97 | self.assertTrue( 98 | size[-1] - size[0] < 300, 99 | f"Save files appear to be growing in size. Sizes: {size}\n" 100 | f"Locations: {locs_bytes}\n" 101 | f"Location keys: {locs_keys}\n" 102 | f"IFP_Objects: {ifp_obj_bytes}\n" 103 | f"IFP_Object keys: {ifp_obj_keys}\n\n" 104 | f"IFP_Object with greatest change in size (delta = {max_obj_delta}):\n\n" 105 | f"{most_changed_obj[0]}\n\n-->\n\n{most_changed_obj[1]}\n\n" 106 | f"Location with greatest change in size (delta = {max_loc_delta}):\n" 107 | f"{most_changed_loc[0]}\n\n-->\n\n{most_changed_loc[1]}", 108 | ) 109 | 110 | self.assertEqual( 111 | initial_obj, latest_obj, "Initial and final loaded data did not match." 112 | ) 113 | 114 | def tearDown(self): 115 | super().tearDown() 116 | os.remove(self.path) 117 | 118 | 119 | class TestSaveLoadNested(IFPTestCase): 120 | def setUp(self): 121 | super().setUp() 122 | FILENAME = f"_ifp_tests_saveload__{uuid.uuid4()}.sav" 123 | 124 | path = os.path.dirname(os.path.realpath(__file__)) 125 | self.path = os.path.join(path, FILENAME) 126 | 127 | self.item1 = Surface(self.game, "table") 128 | self.item2 = Container(self.game, "box") 129 | self.item3 = Container(self.game, "cup") 130 | self.item4 = Thing(self.game, "bean") 131 | self.item5 = Thing(self.game, "spider") 132 | 133 | self.start_room.addThing(self.item1) 134 | self.item1.addThing(self.item2) 135 | self.item2.addThing(self.item3) 136 | self.item3.addThing(self.item4) 137 | self.item2.addThing(self.item5) 138 | 139 | SaveGame(self.game, self.path) 140 | self.start_room.removeThing(self.item1) 141 | self.item1.removeThing(self.item2) 142 | self.item2.removeThing(self.item3) 143 | self.item3.removeThing(self.item4) 144 | self.item2.removeThing(self.item5) 145 | 146 | def test_load(self): 147 | l = LoadGame(self.game, self.path) 148 | self.assertTrue(l.is_valid(), "Save file invalid. Cannot proceed.") 149 | l.load() 150 | 151 | self.assertItemExactlyOnceIn( 152 | self.item1, self.start_room.contains, "Failed to load top level item." 153 | ) 154 | 155 | self.assertItemExactlyOnceIn( 156 | self.item2, self.item1.contains, "Failed to load item nested with depth 1." 157 | ) 158 | 159 | self.assertItemExactlyOnceIn( 160 | self.item3, self.item2.contains, "Failed to load item nested with depth 2." 161 | ) 162 | 163 | self.assertItemExactlyOnceIn( 164 | self.item5, self.item2.contains, "Failed to load item nested with depth 2." 165 | ) 166 | 167 | self.assertItemExactlyOnceIn( 168 | self.item4, self.item3.contains, "Failed to load item nested with depth 3." 169 | ) 170 | 171 | def tearDown(self): 172 | super().tearDown() 173 | os.remove(self.path) 174 | 175 | 176 | class TestSaveLoadComplexAttribute(IFPTestCase): 177 | def setUp(self): 178 | super().setUp() 179 | FILENAME = "_ifp_tests_saveload__0003.sav" 180 | 181 | path = os.path.dirname(os.path.realpath(__file__)) 182 | self.path = os.path.join(path, FILENAME) 183 | 184 | self.item1 = Surface(self.game, "table") 185 | self.item2 = Container(self.game, "box") 186 | 187 | self.EXPECTED_ATTR = { 188 | "data": {"sarah_has_seen": True, "containers": [self.item1],}, 189 | "owner": self.me, 190 | } 191 | 192 | self.item1.custom_attr = self.EXPECTED_ATTR.copy() 193 | 194 | SaveGame(self.game, self.path) 195 | self.item1.custom_attr.clear() 196 | 197 | def test_load(self): 198 | self.assertFalse( 199 | self.item1.custom_attr, 200 | "This test needs custom_attr to be intitially empty.", 201 | ) 202 | 203 | l = LoadGame(self.game, self.path) 204 | self.assertTrue(l.is_valid(), "Save file invalid. Cannot proceed.") 205 | l.load() 206 | 207 | self.assertTrue( 208 | self.item1.custom_attr, 209 | "Loaded save file, but custom attribute still empty.", 210 | ) 211 | self.assertEqual( 212 | self.item1.custom_attr, 213 | self.EXPECTED_ATTR, 214 | "Custom attribute does not match expected", 215 | ) 216 | 217 | def tearDown(self): 218 | super().tearDown() 219 | os.remove(self.path) 220 | 221 | 222 | class TestSaveLoadDaemon(IFPTestCase): 223 | def setUp(self): 224 | super().setUp() 225 | FILENAME = "_ifp_tests_saveload__0004.sav" 226 | 227 | path = os.path.dirname(os.path.realpath(__file__)) 228 | self.path = os.path.join(path, FILENAME) 229 | 230 | self.initial_counter = 0 231 | self.daemon = Daemon(self.game, self._daemon_func) 232 | self.daemon.counter = 0 233 | self.game.daemons.add(self.daemon) 234 | 235 | SaveGame(self.game, self.path) 236 | self.daemon.counter = 67 237 | self.game.daemons.remove(self.daemon) 238 | 239 | def _daemon_func(self, game): 240 | game.addText(f"Turn #{self.daemon.counter}") 241 | self.daemon.counter += 1 242 | 243 | def test_load(self): 244 | self.assertNotEqual( 245 | self.daemon.counter, 246 | self.initial_counter, 247 | "This test needs the daemon counter to start at a different value from " 248 | "The value to load", 249 | ) 250 | self.assertNotIn( 251 | self.daemon, self.game.daemons.active, "Daemon should start as inactive." 252 | ) 253 | 254 | l = LoadGame(self.game, self.path) 255 | self.assertTrue(l.is_valid(), "Save file invalid. Cannot proceed.") 256 | l.load() 257 | 258 | self.assertIn( 259 | self.daemon, 260 | self.game.daemons.active, 261 | "Daemon was not re-added to active after loading.", 262 | ) 263 | self.assertEqual( 264 | self.daemon.counter, 265 | self.initial_counter, 266 | "Daemon counter does not match expected.", 267 | ) 268 | 269 | def tearDown(self): 270 | super().tearDown() 271 | os.remove(self.path) 272 | -------------------------------------------------------------------------------- /tests/things/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RALWORKS/intficpy/3d0e40e1510d4ee07ae13f0cc4ac36926206cdbe/tests/things/__init__.py -------------------------------------------------------------------------------- /tests/things/test_actor.py: -------------------------------------------------------------------------------- 1 | from ..helpers import IFPTestCase 2 | from intficpy.actor import Actor 3 | from intficpy.thing_base import Thing 4 | 5 | 6 | class TestActorInventoryDescription(IFPTestCase): 7 | def setUp(self): 8 | super().setUp() 9 | 10 | self.actor = Actor(self.game, "girl") 11 | self.item = Thing(self.game, "item") 12 | self.item.moveTo(self.actor) 13 | 14 | def test_actor_desc_does_not_include_inventory(self): 15 | self.assertNotIn(self.actor.desc, self.item.verbose_name) 16 | 17 | def test_actor_xdesc_does_not_include_inventory(self): 18 | self.assertNotIn(self.actor.xdesc, self.item.verbose_name) 19 | -------------------------------------------------------------------------------- /tests/things/test_copy_thing.py: -------------------------------------------------------------------------------- 1 | from ..helpers import IFPTestCase 2 | 3 | from intficpy.thing_base import Thing 4 | 5 | 6 | class TestCopyThing(IFPTestCase): 7 | def test_new_thing_has_empty_contains_and_sub_contains(self): 8 | orig = Thing(self.app.game, "bulb") 9 | child = Thing(self.app.game, "seed") 10 | sub_child = Thing(self.app.game, "life") 11 | sub_child.moveTo(child) 12 | child.moveTo(orig) 13 | self.assertTrue(orig.contains) 14 | self.assertTrue(orig.sub_contains) 15 | replica = orig.copyThing() 16 | self.assertFalse(replica.contains) 17 | self.assertFalse(replica.sub_contains) 18 | 19 | 20 | class TestCopyThingUniqueIx(IFPTestCase): 21 | def test_new_thing_has_empty_contains_and_sub_contains(self): 22 | orig = Thing(self.app.game, "bulb") 23 | child = Thing(self.app.game, "seed") 24 | sub_child = Thing(self.app.game, "life") 25 | sub_child.moveTo(child) 26 | child.moveTo(orig) 27 | self.assertTrue(orig.contains) 28 | self.assertTrue(orig.sub_contains) 29 | replica = orig.copyThingUniqueIx() 30 | self.assertFalse(replica.contains) 31 | self.assertFalse(replica.sub_contains) 32 | -------------------------------------------------------------------------------- /tests/things/test_light_source.py: -------------------------------------------------------------------------------- 1 | from ..helpers import IFPTestCase 2 | from intficpy.things import LightSource, Thing 3 | 4 | 5 | class TestDarkness(IFPTestCase): 6 | def setUp(self): 7 | super().setUp() 8 | 9 | self.start_room.dark = True 10 | self.item = Thing(self.game, "item") 11 | self.start_room.addThing(self.item) 12 | 13 | self.light = LightSource(self.game, "light") 14 | 15 | def test_can_see_items_if_room_is_not_dark(self): 16 | self.start_room.dark = False 17 | 18 | self.game.turnMain("l") 19 | desc = self.app.print_stack.pop() 20 | 21 | self.assertIn(self.item.verbose_name, desc) 22 | 23 | def test_cannot_see_items_if_room_is_dark(self): 24 | self.game.turnMain("l") 25 | desc = self.app.print_stack.pop() 26 | 27 | self.assertNotIn(self.item.verbose_name, desc) 28 | 29 | def test_cannot_see_items_if_room_is_dark_and_light_not_lit(self): 30 | self.start_room.addThing(self.light) 31 | self.assertFalse(self.light.is_lit) 32 | 33 | self.game.turnMain("l") 34 | desc = self.app.print_stack.pop() 35 | 36 | self.assertNotIn(self.item.verbose_name, desc) 37 | 38 | def test_can_see_items_if_room_is_dark_and_light_is_lit(self): 39 | self.start_room.addThing(self.light) 40 | self.light.light(self.game) 41 | self.assertTrue(self.light.is_lit) 42 | self.assertFalse(self.start_room.describeDark(self.game)) 43 | 44 | self.game.turnMain("l") 45 | desc = self.app.print_stack.pop() 46 | 47 | self.assertIn(self.item.verbose_name, desc) 48 | 49 | 50 | class TestLightSource(IFPTestCase): 51 | def setUp(self): 52 | super().setUp() 53 | self.light = LightSource(self.game, "light") 54 | 55 | def test_is_lit_state_desc_show_in_desc_when_light_is_lit(self): 56 | self.light.light(self.game) 57 | self.assertIn(self.light.lit_desc, self.light.desc) 58 | 59 | def test_is_lit_state_desc_show_in_desc_when_light_is_not_lit(self): 60 | self.light.light(self.game) 61 | self.light.extinguish(self.game) 62 | self.assertIn(self.light.not_lit_desc, self.light.desc) 63 | -------------------------------------------------------------------------------- /tests/things/test_nested_things.py: -------------------------------------------------------------------------------- 1 | from ..helpers import IFPTestCase 2 | 3 | from intficpy.thing_base import Thing 4 | from intficpy.things import Container, UnderSpace 5 | 6 | 7 | class TestContainer(IFPTestCase): 8 | def test_reveal_contents(self): 9 | box = Container(self.app.game, "box") 10 | widget = Thing(self.app.game, "widget") 11 | widget.moveTo(box) 12 | sub_widget = Thing(self.app.game, "glitter") 13 | sub_widget.moveTo(widget) 14 | box.giveLid() 15 | box.makeClosed() 16 | box.moveTo(self.start_room) 17 | self.assertItemNotIn( 18 | widget, self.start_room.sub_contains, "Contents shown before reveal" 19 | ) 20 | box.makeOpen() 21 | self.assertItemIn( 22 | widget, self.start_room.sub_contains, "Contents not shown after reveal" 23 | ) 24 | self.assertItemIn( 25 | sub_widget, 26 | self.start_room.sub_contains, 27 | "Sub contents not shown after reveal", 28 | ) 29 | 30 | 31 | class TestUnderSpace(IFPTestCase): 32 | def test_reveal_contents(self): 33 | box = UnderSpace(self.app.game, "box") 34 | widget = Thing(self.app.game, "widget") 35 | widget.moveTo(box) 36 | sub_widget = Thing(self.app.game, "glitter") 37 | sub_widget.moveTo(widget) 38 | box.moveTo(self.start_room) 39 | box.revealed = False 40 | self.app.game.turnMain("l") 41 | self.app.game.turnMain("take widget") 42 | self.assertIn("don't see any widget", self.app.print_stack.pop()) 43 | self.assertItemNotIn( 44 | widget, self.start_room.sub_contains, "Contents shown before reveal" 45 | ) 46 | box.revealUnder() 47 | self.assertItemIn( 48 | widget, self.start_room.sub_contains, "Contents not shown after reveal" 49 | ) 50 | self.assertItemIn( 51 | sub_widget, 52 | self.start_room.sub_contains, 53 | "Sub contents not shown after reveal", 54 | ) 55 | -------------------------------------------------------------------------------- /tests/things/test_things.py: -------------------------------------------------------------------------------- 1 | from ..helpers import IFPTestCase 2 | 3 | from intficpy.ifp_object import IFPObject 4 | from intficpy.thing_base import Thing 5 | from intficpy.things import Surface, Container, UnderSpace, Liquid, Lock 6 | from intficpy.actor import Actor 7 | from intficpy.room import Room 8 | 9 | 10 | def make_thing_instantiation_test(thing_class): 11 | def test(self): 12 | item = thing_class(self.game, thing_class.__name__) 13 | self.assertTrue(item.ix) 14 | self.assertIn( 15 | item.ix, 16 | self.game.ifp_objects, 17 | f"Tried to create a {thing_class.__name__}, but index not in " 18 | "things obj_map", 19 | ) 20 | self.assertIs( 21 | self.game.ifp_objects[item.ix], 22 | item, 23 | f"New {thing_class.__name__} index successfully added to " 24 | f"object_map, but {self.game.ifp_objects[item.ix]} found under key instead of " 25 | f"the new instance {item}", 26 | ) 27 | 28 | return test 29 | 30 | 31 | def add_thing_instantiation_tests(): 32 | ignore = [Actor, Liquid, Lock] 33 | thing_classes = Thing.__subclasses__() 34 | for thing_class in thing_classes: 35 | if thing_class in ignore: 36 | continue 37 | func = make_thing_instantiation_test(thing_class) 38 | setattr(TestCreateAllTypes, f"test_create_{thing_class.__name__}", func) 39 | 40 | 41 | class TestCreateAllTypes(IFPTestCase): 42 | def test_create_Liquid(self): 43 | item = Liquid(self.game, Liquid.__name__, "water") 44 | self.assertTrue(item.ix) 45 | self.assertIn( 46 | item.ix, 47 | self.game.ifp_objects, 48 | f"Tried to create a {Liquid.__name__}, but index not in things obj_map", 49 | ) 50 | self.assertIs( 51 | self.game.ifp_objects[item.ix], 52 | item, 53 | f"New {Liquid.__name__} index successfully added to " 54 | f"object_map, but {self.game.ifp_objects[item.ix]} found under key instead of " 55 | f"the new instance {item}", 56 | ) 57 | 58 | def test_create_Actor(self): 59 | item = Actor(self.game, Actor.__name__) 60 | self.assertTrue(item.ix) 61 | self.assertIn( 62 | item.ix, 63 | self.game.ifp_objects, 64 | f"Tried to create a {Actor.__name__}, but index not in things obj_map", 65 | ) 66 | self.assertIs( 67 | self.game.ifp_objects[item.ix], 68 | item, 69 | f"New {Actor.__name__} index successfully added to " 70 | f"object_map, but {self.game.ifp_objects[item.ix]} found under key instead of " 71 | f"the new instance {item}", 72 | ) 73 | 74 | def test_create_Lock(self): 75 | item = Lock(self.game, self.game, Lock.__name__, None) 76 | self.assertTrue(item.ix) 77 | self.assertIn( 78 | item.ix, 79 | self.game.ifp_objects, 80 | f"Tried to create a {Lock.__name__}, but index not in things obj_map", 81 | ) 82 | self.assertIs( 83 | self.game.ifp_objects[item.ix], 84 | item, 85 | f"New {Lock.__name__} index successfully added to " 86 | f"object_map, but {self.game.ifp_objects[item.ix]} found under key instead of " 87 | f"the new instance {item}", 88 | ) 89 | 90 | 91 | class TestAddRemoveThing(IFPTestCase): 92 | def describe(self, subject): 93 | if isinstance(subject, Room): 94 | self.game.turnMain("l") 95 | return self.app.print_stack.pop() 96 | return subject.desc 97 | 98 | def _assert_can_add_remove(self, parent, child): 99 | self.assertNotIn(child.ix, parent.contains) 100 | 101 | parent.desc_reveal = True 102 | 103 | self.assertNotIn( 104 | child.verbose_name, 105 | self.describe(parent), 106 | "This test needs the child verbose_name to not intially be in " 107 | "the parent description", 108 | ) 109 | 110 | if child.lock_obj: 111 | self.assertNotIn(child.lock_obj.ix, parent.contains) 112 | for sub_item in child.children: 113 | self.assertNotIn(sub_item.ix, parent.contains) 114 | 115 | parent.addThing(child) 116 | self.assertItemIn( 117 | child, 118 | parent.contains, 119 | f"Tried to add item to {parent}, but item not found in `contains`", 120 | ) 121 | 122 | if child.lock_obj: 123 | self.assertItemIn( 124 | child.lock_obj, 125 | parent.contains, 126 | f"Added item with lock to {parent}, but lock_obj not found in `contains`", 127 | ) 128 | 129 | for sub_item in child.children: 130 | self.assertItemIn( 131 | sub_item, 132 | parent.contains, 133 | f"Tried to add item to {parent}, but composite child item not found in " 134 | "`contains`", 135 | ) 136 | 137 | parent.removeThing(child) 138 | self.assertItemNotIn( 139 | child, 140 | parent.contains, 141 | f"Tried to remove item from {parent}, but item still found in `contains`", 142 | ) 143 | if child.lock_obj: 144 | self.assertNotIn( 145 | child.lock_obj.ix, 146 | parent.contains, 147 | f"lock_obj {child.lock_obj} not removed from {parent}", 148 | ) 149 | for sub_item in child.children: 150 | self.assertNotIn( 151 | sub_item.ix, 152 | parent.contains, 153 | f"composite child {sub_item} not removed from {parent}", 154 | ) 155 | 156 | def test_add_remove_from_Surface(self): 157 | parent = Surface(self.game, "parent") 158 | child = Thing(self.game, "child") 159 | self.start_room.addThing(parent) 160 | self._assert_can_add_remove(parent, child) 161 | 162 | def test_add_remove_from_Container(self): 163 | parent = Container(self.game, "parent") 164 | child = Thing(self.game, "child") 165 | self.start_room.addThing(parent) 166 | self._assert_can_add_remove(parent, child) 167 | 168 | def test_add_remove_from_UnderSpace(self): 169 | parent = UnderSpace(self.game, "parent") 170 | parent.revealed = True 171 | child = Thing(self.game, "child") 172 | self.start_room.addThing(parent) 173 | self._assert_can_add_remove(parent, child) 174 | 175 | def test_add_remove_from_Room(self): 176 | parent = Room(self.game, "parent", "This is a room. ") 177 | child = Thing(self.game, "child") 178 | self._assert_can_add_remove(parent, child) 179 | 180 | def test_add_remove_composite_item_from_Surface(self): 181 | parent = Surface(self.game, "parent") 182 | child = Thing(self.game, "child") 183 | sub = Thing(self.game, "sub") 184 | child.addComposite(sub) 185 | self.start_room.addThing(parent) 186 | self._assert_can_add_remove(parent, child) 187 | 188 | def test_add_remove_composite_item_from_Container(self): 189 | parent = Container(self.game, "parent") 190 | child = Thing(self.game, "child") 191 | sub = Thing(self.game, "sub") 192 | child.addComposite(sub) 193 | self.start_room.addThing(parent) 194 | self._assert_can_add_remove(parent, child) 195 | 196 | def test_add_remove_composite_item_from_UnderSpace(self): 197 | parent = UnderSpace(self.game, "parent") 198 | parent.revealed = True 199 | child = Thing(self.game, "child") 200 | sub = Thing(self.game, "sub") 201 | child.addComposite(sub) 202 | self.start_room.addThing(parent) 203 | self._assert_can_add_remove(parent, child) 204 | 205 | def test_add_remove_composite_item_from_Room(self): 206 | parent = Room(self.game, "parent", "This is a room. ") 207 | child = Thing(self.game, "child") 208 | sub = Thing(self.game, "sub") 209 | child.addComposite(sub) 210 | self._assert_can_add_remove(parent, child) 211 | 212 | def test_add_remove_item_with_lock_from_Surface(self): 213 | parent = Surface(self.game, "parent") 214 | child = Container(self.game, "child") 215 | child.has_lid = True 216 | lock = Lock(self.game, "lock", None) 217 | child.setLock(lock) 218 | self.start_room.addThing(parent) 219 | self._assert_can_add_remove(parent, child) 220 | 221 | def test_add_remove_item_with_lock_from_Container(self): 222 | parent = Container(self.game, "parent") 223 | child = Container(self.game, "child") 224 | child.has_lid = True 225 | lock = Lock(self.game, "lock", None) 226 | child.setLock(lock) 227 | self.start_room.addThing(parent) 228 | self._assert_can_add_remove(parent, child) 229 | 230 | def test_add_remove_item_with_lock_from_UnderSpace(self): 231 | parent = UnderSpace(self.game, "parent") 232 | parent.revealed = True 233 | child = Container(self.game, "child") 234 | child.has_lid = True 235 | lock = Lock(self.game, "lock", None) 236 | child.setLock(lock) 237 | self.start_room.addThing(parent) 238 | self._assert_can_add_remove(parent, child) 239 | 240 | def test_add_remove_item_with_lock_from_Room(self): 241 | parent = Room(self.game, "parent", "This is a room. ") 242 | parent.revealed = True 243 | child = Container(self.game, "child") 244 | child.has_lid = True 245 | lock = Lock(self.game, "lock", None) 246 | child.setLock(lock) 247 | self._assert_can_add_remove(parent, child) 248 | 249 | 250 | class TestMoveTo(IFPTestCase): 251 | def test_move_to_removes_item_from_old_location_and_adds_to_new_location(self): 252 | old = Room(self.game, "old", "It is old") 253 | child = Thing(self.game, "child") 254 | old.addThing(child) 255 | 256 | new = Container(self.game, "new") 257 | child.moveTo(new) 258 | 259 | self.assertItemIn(child, new.contains, "Item not added to new location") 260 | self.assertItemNotIn(child, old.contains, "Item not removed from old location") 261 | self.assertIs(child.location, new, "Item not added to new location") 262 | 263 | def test_move_to_removes_item_from_old_superlocation_subcontains(self): 264 | room = Room(self.game, "old", "It is old") 265 | old = Container(self.game, "box") 266 | room.addThing(old) 267 | child = Thing(self.game, "child") 268 | old.addThing(child) 269 | 270 | new = Container(self.game, "new") 271 | 272 | self.assertItemIn( 273 | child, room.sub_contains, "Item not removed from old location" 274 | ) 275 | 276 | child.moveTo(new) 277 | 278 | self.assertItemNotIn( 279 | child, room.sub_contains, "Item not removed from old location" 280 | ) 281 | self.assertIs(child.location, new, "Item not added to new location") 282 | 283 | def test_adds_to_new_location_if_no_previous_location(self): 284 | child = Thing(self.game, "child") 285 | 286 | new = Container(self.game, "new") 287 | child.moveTo(new) 288 | 289 | self.assertItemIn(child, new.contains, "Item not added to new location") 290 | self.assertIs(child.location, new, "Item not added to new location") 291 | 292 | def test_move_item_to_nested_adding_container_first_then_item(self): 293 | container = Container(self.game, "cup") 294 | item = Thing(self.game, "bead") 295 | 296 | container.moveTo(self.start_room) 297 | self.assertIs(container.location, self.start_room) 298 | 299 | item.moveTo(container) 300 | 301 | self.assertIs(item.location, container) 302 | self.assertTrue(container.topLevelContainsItem(item)) 303 | 304 | def test_move_item_to_nested_adding_item_first_then_container(self): 305 | container = Container(self.game, "cup") 306 | item = Thing(self.game, "bead") 307 | 308 | item.moveTo(container) 309 | self.assertIs( 310 | item.location, 311 | container, 312 | "Move item to container failed to set item location when container location was None", 313 | ) 314 | 315 | container.moveTo(self.start_room) 316 | 317 | self.assertIs(item.location, container) 318 | self.assertTrue(container.topLevelContainsItem(item)) 319 | 320 | def test_move_nested_item_between_locations(self): 321 | room2 = Room(self.game, "room", "here") 322 | container = Container(self.game, "cup") 323 | item = Thing(self.game, "bead") 324 | 325 | item.moveTo(container) 326 | container.moveTo(self.start_room) 327 | 328 | self.assertIs(container.location, self.start_room) 329 | self.assertIs(item.location, container) 330 | self.assertTrue(container.topLevelContainsItem(item)) 331 | 332 | container.moveTo(room2) 333 | 334 | self.assertIs(container.location, room2) 335 | self.assertIs( 336 | item.location, 337 | container, 338 | "Nested item location was updated when container was moved", 339 | ) 340 | self.assertTrue(container.topLevelContainsItem(item)) 341 | 342 | 343 | add_thing_instantiation_tests() 344 | -------------------------------------------------------------------------------- /tests/verbs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RALWORKS/intficpy/3d0e40e1510d4ee07ae13f0cc4ac36926206cdbe/tests/verbs/__init__.py -------------------------------------------------------------------------------- /tests/verbs/test_clothing_verbs.py: -------------------------------------------------------------------------------- 1 | from ..helpers import IFPTestCase 2 | 3 | from intficpy.things import Thing, Clothing 4 | 5 | 6 | class TestDoff(IFPTestCase): 7 | def test_doff_player_not_wearing_gives_player_not_wearing_message(self): 8 | item = Thing(self.game, "item") 9 | item.moveTo(self.start_room) 10 | self.game.turnMain("doff item") 11 | self.assertIn( 12 | "aren't wearing", 13 | self.app.print_stack.pop(), 14 | "Did not receive expected 'not wearing' scope message", 15 | ) 16 | 17 | def test_doff_player_wearing_doffs_item(self): 18 | item = Clothing(self.game, "item") 19 | self.me.wearing[item.ix] = [item] 20 | self.game.turnMain("doff item") 21 | self.assertIn("You take off", self.app.print_stack.pop()) 22 | self.assertItemNotIn(item, self.me.wearing, "Item not removed from wearing") 23 | self.assertItemIn(item, self.me.contains, "Item not added to main inv") 24 | -------------------------------------------------------------------------------- /tests/verbs/test_drop_verb.py: -------------------------------------------------------------------------------- 1 | from ..helpers import IFPTestCase 2 | 3 | from intficpy.things import Thing, Container, Liquid 4 | 5 | 6 | class TestDropVerb(IFPTestCase): 7 | def test_verb_func_drops_item(self): 8 | item = Thing(self.game, self._get_unique_noun()) 9 | item.invItem = True 10 | self.me.addThing(item) 11 | self.assertIn(item.ix, self.me.contains) 12 | self.assertEqual(len(self.me.contains[item.ix]), 1) 13 | self.assertIn(item, self.me.contains[item.ix]) 14 | 15 | self.game.turnMain(f"drop {item.verbose_name}") 16 | 17 | self.assertItemNotIn( 18 | item, self.me.contains, "Dropped item, but item still in inventory" 19 | ) 20 | 21 | def test_drop_item_not_in_inv(self): 22 | item = Thing(self.game, "shoe") 23 | item.invItem = True 24 | self.start_room.addThing(item) 25 | self.assertFalse(self.me.containsItem(item)) 26 | 27 | self.game.turnMain(f"drop {item.verbose_name}") 28 | self.assertIn("You are not holding", self.app.print_stack.pop()) 29 | 30 | def test_drop_liquid_in_container(self): 31 | cup = Container(self.game, "cup") 32 | water = Liquid(self.game, "water", "water") 33 | water.moveTo(cup) 34 | cup.moveTo(self.me) 35 | self.game.turnMain("drop water") 36 | self.assertIn("You drop the cup", self.app.print_stack.pop()) 37 | self.assertFalse(self.game.me.containsItem(cup)) 38 | self.assertTrue(cup.containsItem(water)) 39 | 40 | def test_drop_composite_child(self): 41 | machine = Thing(self.game, "machine") 42 | wheel = Thing(self.game, "wheel") 43 | machine.addComposite(wheel) 44 | machine.moveTo(self.me) 45 | self.game.turnMain("drop wheel") 46 | self.assertIn("wheel is attached to the machine", self.app.print_stack.pop()) 47 | self.assertTrue(self.me.containsItem(wheel)) 48 | -------------------------------------------------------------------------------- /tests/verbs/test_get.py: -------------------------------------------------------------------------------- 1 | from ..helpers import IFPTestCase 2 | 3 | from intficpy.thing_base import Thing 4 | from intficpy.things import Surface, Container, Liquid, UnderSpace, Lock 5 | from intficpy.verb import GetVerb 6 | 7 | 8 | class TestGetVerb(IFPTestCase): 9 | def test_verb_func_adds_invitem_to_inv(self): 10 | item = Thing(self.game, self._get_unique_noun()) 11 | item.invItem = True 12 | self.start_room.addThing(item) 13 | 14 | success = GetVerb()._runVerbFuncAndEvents(self.game, item) 15 | self.assertTrue(success) 16 | 17 | self.assertIn(item.ix, self.me.contains) 18 | self.assertEqual(len(self.me.contains[item.ix]), 1) 19 | self.assertIn(item, self.me.contains[item.ix]) 20 | 21 | def test_verb_func_does_not_add_to_inv_where_invitem_false(self): 22 | item = Thing(self.game, self._get_unique_noun()) 23 | item.invItem = False 24 | self.start_room.addThing(item) 25 | 26 | self.assertFalse(item.ix in self.me.contains) 27 | 28 | success = GetVerb()._runVerbFuncAndEvents(self.game, item) 29 | self.assertFalse(success) 30 | 31 | self.assertNotIn(item.ix, self.me.contains) 32 | 33 | def test_verb_func_does_not_add_to_inv_where_already_in_inv(self): 34 | item = Thing(self.game, self._get_unique_noun()) 35 | item.invItem = True 36 | self.me.addThing(item) 37 | self.assertTrue(item.ix in self.me.contains) 38 | self.assertEqual(len(self.me.contains[item.ix]), 1) 39 | self.assertIn(item, self.me.contains[item.ix]) 40 | 41 | success = GetVerb()._runVerbFuncAndEvents(self.game, item) 42 | self.assertFalse(success) 43 | 44 | self.assertEqual(len(self.me.contains[item.ix]), 1) 45 | 46 | def test_get_item_when_pc_is_on_surface(self): 47 | loc = Surface(self.game, "desk") 48 | loc.invItem = True 49 | loc.moveTo(self.start_room) 50 | loc.can_contain_standing_player = True 51 | 52 | sub_loc = Container(self.game, "box") 53 | sub_loc.moveTo(loc) 54 | sub_loc.can_contain_standing_player = True 55 | 56 | self.game.me.moveTo(sub_loc) 57 | 58 | self.game.turnMain("take desk") 59 | 60 | self.assertIn("You take the desk", self.app.print_stack.pop()) 61 | self.assertIn("You get off of the desk", self.app.print_stack.pop()) 62 | self.assertIn("You get out of the box", self.app.print_stack.pop()) 63 | 64 | def test_get_item_when_pc_sitting(self): 65 | item = Thing(self.game, "bob") 66 | item.moveTo(self.start_room) 67 | self.game.me.position = "sitting" 68 | 69 | self.game.turnMain("take bob") 70 | 71 | self.assertIn("You stand up. ", self.app.print_stack) 72 | self.assertEqual(self.game.me.position, "standing") 73 | 74 | def test_get_liquid_in_container_gets_container(self): 75 | container = Container(self.game, "cup") 76 | container.invItem = True 77 | container.moveTo(self.start_room) 78 | item = Liquid(self.game, "broth", "broth") 79 | item.moveTo(container) 80 | 81 | self.game.turnMain("take broth") 82 | 83 | self.assertIn("You take the cup. ", self.app.print_stack) 84 | 85 | def test_get_thing_nested_in_thing_in_inventory(self): 86 | container = Container(self.game, "cup") 87 | container.moveTo(self.start_room) 88 | item = Thing(self.game, "bead") 89 | item.invItem = True 90 | 91 | container.moveTo(self.game.me) 92 | item.moveTo(container) 93 | 94 | self.game.turnMain("look in cup") 95 | 96 | self.game.turnMain("take bead") 97 | 98 | self.assertEqual( 99 | "You remove the bead from the cup. ", self.app.print_stack.pop() 100 | ) 101 | 102 | def test_get_explicitly_invitem_component_of_composite_object(self): 103 | parent = Thing(self.game, "cube") 104 | child = Thing(self.game, "knob") 105 | parent.addComposite(child) 106 | parent.moveTo(self.start_room) 107 | child.invItem = True 108 | 109 | self.game.turnMain("take knob") 110 | self.assertIn("The knob is attached to the cube. ", self.app.print_stack) 111 | 112 | def test_get_underspace_reveals_single_contained_item(self): 113 | parent = UnderSpace(self.game, "rug") 114 | parent.invItem = True 115 | child = Thing(self.game, "penny") 116 | parent.moveTo(self.start_room) 117 | child.moveTo(parent) 118 | 119 | self.game.turnMain("take rug") 120 | self.assertIn("A penny is revealed. ", self.app.print_stack) 121 | 122 | def test_get_underspace_reveals_multiple_contained_items(self): 123 | parent = UnderSpace(self.game, "rug") 124 | parent.invItem = True 125 | child = Thing(self.game, "penny") 126 | child2 = Thing(self.game, "rock") 127 | parent.moveTo(self.start_room) 128 | child.moveTo(parent) 129 | child2.moveTo(parent) 130 | 131 | self.game.turnMain("take rug") 132 | self.assertIn("are revealed", self.app.print_stack.pop()) 133 | 134 | def test_get_composite_underspace_reveals_contained_item(self): 135 | item = Container(self.game, "box") 136 | item.invItem = True 137 | parent = UnderSpace(self.game, "space") 138 | child = Thing(self.game, "penny") 139 | item.addComposite(parent) 140 | item.moveTo(self.start_room) 141 | child.moveTo(parent) 142 | 143 | self.game.turnMain("take box") 144 | self.assertIn("A penny is revealed. ", self.app.print_stack) 145 | 146 | def test_get_object_from_closed_container_in_inventory(self): 147 | box = Container(self.game, "box") 148 | box.giveLid() 149 | item = Thing(self.game, "bead") 150 | item.invItem = True 151 | item.moveTo(box) 152 | box.makeOpen() 153 | box.moveTo(self.game.me) 154 | 155 | self.assertFalse(self.game.me.topLevelContainsItem(item)) 156 | self.game.turnMain("look in box") 157 | self.assertIn("bead", self.app.print_stack.pop()) 158 | self.game.turnMain("close box") 159 | self.assertFalse(box.is_open, "Failed to close box") 160 | self.game.turnMain("take bead") 161 | self.assertTrue(self.game.me.topLevelContainsItem(item)) 162 | 163 | def test_get_object_from_closed_and_locked_container(self): 164 | box = Container(self.game, "box") 165 | box.giveLid() 166 | item = Thing(self.game, "bead") 167 | item.invItem = True 168 | item.moveTo(box) 169 | box.setLock(Lock(self.game, True, None)) 170 | box.revealed = True 171 | box.moveTo(self.start_room) 172 | 173 | self.game.turnMain("get bead") 174 | self.assertIn("is locked", self.app.print_stack.pop()) 175 | 176 | self.assertTrue(box.containsItem(item)) 177 | self.assertFalse(box.is_open) 178 | 179 | 180 | class TestTakeAll(IFPTestCase): 181 | def test_take_all_takes_all_known_top_level_invitems(self): 182 | hat = Thing(self.game, "hat") 183 | hat.invItem = True 184 | hat.moveTo(self.start_room) 185 | cat = Thing(self.game, "cat") 186 | cat.invItem = True 187 | cat.moveTo(self.start_room) 188 | 189 | self.game.turnMain("l") 190 | self.game.turnMain("take all") 191 | 192 | self.assertTrue(self.game.me.containsItem(hat)) 193 | self.assertTrue(self.game.me.containsItem(cat)) 194 | 195 | def test_no_items_are_taken_unless_they_are_known(self): 196 | hat = Thing(self.game, "hat") 197 | hat.invItem = True 198 | hat.moveTo(self.start_room) 199 | cat = Thing(self.game, "cat") 200 | cat.invItem = True 201 | cat.moveTo(self.start_room) 202 | 203 | # self.game.turnMain("l") # we haven't looked, so we don't know 204 | self.game.turnMain("take all") 205 | 206 | self.assertFalse(self.game.me.containsItem(hat)) 207 | self.assertFalse(self.game.me.containsItem(cat)) 208 | 209 | def test_take_all_takes_known_objects_from_sub_locations(self): 210 | desk = Surface(self.game, "desk") 211 | desk.desc_reveal = True 212 | desk.moveTo(self.start_room) 213 | hat = Thing(self.game, "hat") 214 | hat.invItem = True 215 | hat.moveTo(desk) 216 | 217 | self.game.turnMain("l") 218 | self.game.turnMain("take all") 219 | 220 | self.assertTrue(self.game.me.containsItem(hat)) 221 | 222 | def test_take_all_does_not_take_items_that_are_not_discovered(self): 223 | desk = Surface(self.game, "desk") 224 | desk.invItem = False 225 | desk.desc_reveal = False # don't reveal the contents with "look" 226 | desk.moveTo(self.start_room) 227 | hat = Thing(self.game, "hat") 228 | hat.invItem = True 229 | hat.moveTo(desk) 230 | 231 | self.game.turnMain("l") 232 | self.game.turnMain("take all") 233 | 234 | self.assertFalse(self.game.me.containsItem(hat)) 235 | 236 | 237 | class TestRemoveFrom(IFPTestCase): 238 | def test_remove_me(self): 239 | box = Container(self.game, "box") 240 | box.moveTo(self.start_room) 241 | self.game.me.moveTo(box) 242 | self.game.turnMain("remove me from box") 243 | self.assertIn("cannot take yourself", self.app.print_stack.pop()) 244 | 245 | def test_remove_object_from_something_the_object_is_not_in(self): 246 | box = Container(self.game, "box") 247 | box.moveTo(self.start_room) 248 | glob = Thing(self.game, "glob") 249 | glob.moveTo(self.start_room) 250 | self.game.turnMain("remove glob from box") 251 | self.assertIn("The glob is not in the box. ", self.app.print_stack) 252 | 253 | def test_remove_object_already_in_top_level_inventory(self): 254 | glob = Thing(self.game, "glob") 255 | glob.moveTo(self.me) 256 | self.game.turnMain("remove glob from me") 257 | self.assertIn("You are currently holding the glob. ", self.app.print_stack) 258 | 259 | def test_remove_object_from_locked_closed_container(self): 260 | box = Container(self.game, "box") 261 | box.moveTo(self.start_room) 262 | box.giveLid() 263 | box.setLock(Lock(self.game, True, None)) 264 | box.revealed = True 265 | glob = Thing(self.game, "glob") 266 | glob.moveTo(box) 267 | self.game.turnMain("remove glob from box") 268 | self.assertIn("The box is locked. ", self.app.print_stack) 269 | self.assertIs(glob.location, box) 270 | 271 | def test_remove_non_inv_item(self): 272 | box = Container(self.game, "box") 273 | box.moveTo(self.start_room) 274 | glob = Thing(self.game, "glob") 275 | glob.invItem = False 276 | glob.moveTo(box) 277 | self.game.turnMain("remove glob from box") 278 | self.assertIn(glob.cannotTakeMsg, self.app.print_stack) 279 | self.assertIs(glob.location, box) 280 | 281 | def test_remove_child_composite_object(self): 282 | box = Container(self.game, "box") 283 | box.moveTo(self.start_room) 284 | glob = Thing(self.game, "glob") 285 | glob.moveTo(box) 286 | drip = Thing(self.game, "drip") 287 | glob.addComposite(drip) 288 | self.game.turnMain("remove drip from box") 289 | self.assertIn(drip.cannotTakeMsg, self.app.print_stack) 290 | self.assertIs(drip.location, box) 291 | 292 | def test_remove_object_containing_player(self): 293 | pedestal = Surface(self.game, "pedestal") 294 | pedestal.moveTo(self.start_room) 295 | box = Container(self.game, "box") 296 | box.invItem = True 297 | box.moveTo(pedestal) 298 | self.me.moveTo(box) 299 | self.game.turnMain("remove box from pedestal") 300 | self.assertIn("You are currently in the box", self.app.print_stack.pop()) 301 | self.assertIs(box.location, pedestal) 302 | 303 | def test_remove_underspace_not_containing_items(self): 304 | box = Container(self.game, "box") 305 | box.moveTo(self.start_room) 306 | rug = UnderSpace(self.game, "rug") 307 | rug.invItem = True 308 | rug.moveTo(box) 309 | self.game.turnMain("remove rug from box") 310 | msg = self.app.print_stack.pop() 311 | self.assertNotIn("revealed", msg) 312 | 313 | def test_remove_underspace_containing_items(self): 314 | box = Container(self.game, "box") 315 | box.moveTo(self.start_room) 316 | rug = UnderSpace(self.game, "rug") 317 | rug.invItem = True 318 | rug.moveTo(box) 319 | penny = Thing(self.game, "penny") 320 | bead = Thing(self.game, "bead") 321 | penny.moveTo(rug) 322 | bead.moveTo(rug) 323 | self.game.turnMain("remove rug from box") 324 | msg = self.app.print_stack.pop() 325 | self.assertIn("penny", msg) 326 | self.assertIn("bead", msg) 327 | self.assertIn("are revealed", msg) 328 | self.assertIs(penny.location, box) 329 | self.assertIs(bead.location, box) 330 | 331 | def test_remove_item_with_component_underspace_containing_items(self): 332 | box = Container(self.game, "box") 333 | box.moveTo(self.start_room) 334 | mishmash = Thing(self.game, "mishmash") 335 | mishmash.invItem = True 336 | rug = UnderSpace(self.game, "rug") 337 | mishmash.addComposite(rug) 338 | mishmash.moveTo(box) 339 | penny = Thing(self.game, "penny") 340 | penny.moveTo(rug) 341 | self.game.turnMain("remove mishmash from box") 342 | msg = self.app.print_stack.pop() 343 | self.assertIn("penny", msg) 344 | self.assertIn("is revealed", msg) 345 | self.assertIs(penny.location, box) 346 | -------------------------------------------------------------------------------- /tests/verbs/test_get_all_drop_all.py: -------------------------------------------------------------------------------- 1 | from ..helpers import IFPTestCase 2 | 3 | from intficpy.thing_base import Thing 4 | 5 | 6 | class TestInventoryVerbs(IFPTestCase): 7 | def test_get_all_drop(self): 8 | item1 = Thing(self.game, "miracle") 9 | item2 = Thing(self.game, "wonder") 10 | item1.invItem = True 11 | item2.invItem = True 12 | item3 = item2.copyThing() 13 | item1.makeKnown(self.me) 14 | item3.makeKnown(self.me) 15 | self.start_room.addThing(item1) 16 | self.start_room.addThing(item3) 17 | self.me.addThing(item2) 18 | 19 | self.assertNotIn(item1.ix, self.me.contains) 20 | self.assertIn(item2.ix, self.me.contains) 21 | self.assertNotIn(item3, self.me.contains[item2.ix]) 22 | 23 | self.game.turnMain("take all") 24 | getall_msg = self.app.print_stack.pop() 25 | 26 | self.assertIn( 27 | item1.ix, 28 | self.me.contains, 29 | f"Item not added to inv with get all. Msg: '{getall_msg}'", 30 | ) 31 | self.assertIn(item1, self.me.contains[item1.ix]) 32 | self.assertIn( 33 | item2.ix, 34 | self.me.contains, 35 | f"Item not added to inv with get all. Msg: '{getall_msg}'", 36 | ) 37 | self.assertIn(item2, self.me.contains[item2.ix]) 38 | 39 | self.game.turnMain("take all") 40 | getall_msg = self.app.print_stack.pop() 41 | self.assertEqual(getall_msg, "There are no obvious items here to take. ") 42 | 43 | def test_drop_all(self): 44 | item1 = Thing(self.game, "miracle") 45 | item2 = Thing(self.game, "wonder") 46 | item1.invItem = True 47 | item2.invItem = True 48 | item1.makeKnown(self.me) 49 | item2.makeKnown(self.me) 50 | self.me.addThing(item1) 51 | self.me.addThing(item2) 52 | 53 | self.assertIs( 54 | self.me.location, 55 | self.start_room, 56 | "This test needs the Player to be in the start room", 57 | ) 58 | 59 | self.game.turnMain("drop all") 60 | dropall_msg = self.app.print_stack.pop() 61 | 62 | self.assertEqual( 63 | len(self.me.contains), 64 | 0, 65 | f"Expected empty inv, but found {self.me.contains}", 66 | ) 67 | 68 | self.assertIn(item1.ix, self.start_room.contains) 69 | self.assertIn(item1, self.start_room.contains[item1.ix]) 70 | self.assertIn(item2.ix, self.start_room.contains) 71 | self.assertIn(item2, self.start_room.contains[item2.ix]) 72 | self.game.turnMain("drop all") 73 | dropall_msg = self.app.print_stack.pop() 74 | self.assertEqual(dropall_msg, "Your inventory is empty. ") 75 | -------------------------------------------------------------------------------- /tests/verbs/test_help_and_score.py: -------------------------------------------------------------------------------- 1 | from ..helpers import IFPTestCase 2 | 3 | 4 | class TestHelpVerb(IFPTestCase): 5 | def test_help_prints_help_msg(self): 6 | self.game.turnMain("help") 7 | 8 | self.assertIn("Type INSTRUCTIONS", self.app.print_stack.pop()) 9 | 10 | def test_instructions_prints_instructions(self): 11 | self.game.turnMain("instructions") 12 | 13 | self.assertIn(self.game.aboutGame.basic_instructions, self.app.print_stack) 14 | 15 | 16 | class TestScoreVerb(IFPTestCase): 17 | def test_help_prints_current_score(self): 18 | self.game.turnMain("score") 19 | 20 | self.assertIn("0 points", self.app.print_stack.pop()) 21 | 22 | 23 | class TestFullScoreVerb(IFPTestCase): 24 | def test_help_prints_current_score(self): 25 | self.game.turnMain("fullscore") 26 | 27 | self.assertIn("You haven't scored any points", self.app.print_stack.pop()) 28 | 29 | 30 | class TestVerbsVerb(IFPTestCase): 31 | def test_verbs_verb_prints_verbs(self): 32 | self.game.turnMain("verbs") 33 | 34 | self.assertIn("For help with phrasing", self.app.print_stack.pop()) 35 | # for now, make sure we are printing *some* verbs at least 36 | self.assertIn("set on", self.app.print_stack.pop()) 37 | self.assertIn("accepts the following basic verbs", self.app.print_stack.pop()) 38 | 39 | 40 | class TestVerbHelpVerb(IFPTestCase): 41 | def test_verb_help_ask_verb(self): 42 | self.game.turnMain("verb help ask") 43 | self.assertIn("ask (person) about (thing)", self.app.print_stack.pop()) 44 | 45 | def test_verb_help_go_verb(self): 46 | self.game.turnMain("verb help go") 47 | self.assertIn("go (direction)", self.app.print_stack.pop()) 48 | 49 | def test_verb_help_lead_verb(self): 50 | self.game.turnMain("verb help lead") 51 | self.assertIn("lead (person) (direction)", self.app.print_stack.pop()) 52 | 53 | def test_verb_help_nonexistent_verb(self): 54 | self.game.turnMain("verb help nevernevernevernevernononono") 55 | self.assertIn("I found no verb corresponding to", self.app.print_stack.pop()) 56 | -------------------------------------------------------------------------------- /tests/verbs/test_hint.py: -------------------------------------------------------------------------------- 1 | from intficpy.score import HintNode, Hint 2 | 3 | from ..helpers import IFPTestCase 4 | 5 | 6 | class TestHintVerb(IFPTestCase): 7 | def test_hint_call_with_no_hint_gives_no_hint_message(self): 8 | self.game.turnMain("hint") 9 | self.assertIn("no hints currently available", self.app.print_stack.pop()) 10 | 11 | def test_hint_call_with_available_hints_gives_next_hint(self): 12 | HINT_TEXT = "Try this!" 13 | hint = Hint(self.game, HINT_TEXT) 14 | node = HintNode(self.game, [hint]) 15 | self.game.hints.setNode(self.game, node) 16 | self.game.turnMain("hint") 17 | self.assertIn("1/1", self.app.print_stack.pop()) 18 | self.assertIn(HINT_TEXT, self.app.print_stack.pop()) 19 | -------------------------------------------------------------------------------- /tests/verbs/test_inventory.py: -------------------------------------------------------------------------------- 1 | from ..helpers import IFPTestCase 2 | 3 | from intficpy.things import ( 4 | Thing, 5 | Container, 6 | Clothing, 7 | ) 8 | 9 | 10 | class TestEmptyInventory(IFPTestCase): 11 | def test_view_empty_inv(self): 12 | self.game.turnMain("drop all") 13 | self.assertEqual( 14 | len(self.me.contains), 0, "This test requires an empty player inventory" 15 | ) 16 | 17 | EMPTY_INV_MSG = "You don't have anything with you. " 18 | 19 | self.game.turnMain("i") 20 | inv_msg = self.app.print_stack.pop() 21 | 22 | self.assertEqual( 23 | inv_msg, 24 | EMPTY_INV_MSG, 25 | "Viewed empty inventory. Message does not match expected. ", 26 | ) 27 | 28 | 29 | class TestFullInventory(IFPTestCase): 30 | def setUp(self): 31 | super().setUp() 32 | self.parent = Thing(self.game, "cube") 33 | self.child = Container(self.game, "slot") 34 | self.container = Container(self.game, "box") 35 | self.nested_item = Thing(self.game, "bead") 36 | self.nested_item.moveTo(self.container) 37 | self.parent.addComposite(self.child) 38 | self.stacked1 = Thing(self.game, "tile") 39 | self.stacked2 = self.stacked1.copyThing() 40 | self.clothing = Clothing(self.game, "scarf") 41 | self.clothing1 = Clothing(self.game, "mitten") 42 | self.clothing2 = self.clothing1.copyThing() 43 | self.clothing3 = Clothing(self.game, "hat") 44 | 45 | self.me.addThing(self.parent) 46 | self.me.addThing(self.child) 47 | self.me.addThing(self.container) 48 | self.me.addThing(self.stacked1) 49 | self.me.addThing(self.stacked2) 50 | self.me.addThing(self.clothing) 51 | self.me.addThing(self.clothing1) 52 | self.me.addThing(self.clothing2) 53 | self.me.addThing(self.clothing3) 54 | 55 | self.game.turnMain("wear scarf") 56 | self.game.turnMain("wear hat") 57 | self.game.turnMain("wear mitten") 58 | self.game.turnMain("wear mitten") 59 | 60 | def strip_desc(self, desc): 61 | desc = desc.replace(". ", "").replace(",", "").replace("and", "") 62 | return " ".join(desc.split()) 63 | 64 | def test_inventory_items_desc(self): 65 | BASE_MSG = "You have" 66 | 67 | self.game.turnMain("i") 68 | self.app.print_stack.pop() 69 | inv_desc = self.app.print_stack.pop() 70 | 71 | self.assertIn("You have", inv_desc) 72 | 73 | stacked_desc = ( 74 | f"{len(self.me.contains[self.stacked1.ix])} {self.stacked1.plural}" 75 | ) 76 | self.assertIn( 77 | stacked_desc, inv_desc, "Stacked item description should be in inv desc" 78 | ) 79 | 80 | self.assertIn( 81 | "a cube", inv_desc, 82 | ) 83 | 84 | self.assertIn("a box (in the box is a bead.)", inv_desc) 85 | 86 | def test_inventory_wearing_desc(self): 87 | BASE_MSG = "You are wearing" 88 | 89 | self.game.turnMain("i") 90 | wearing_desc = self.app.print_stack.pop() 91 | 92 | self.assertIn( 93 | "a hat", wearing_desc, "Clothing item description should be in inv desc", 94 | ) 95 | self.assertIn( 96 | "a scarf", wearing_desc, "Clothing item description should be in inv desc", 97 | ) 98 | self.assertIn( 99 | "mittens", 100 | wearing_desc, 101 | "Stacked clothing item description should be in inv desc", 102 | ) 103 | -------------------------------------------------------------------------------- /tests/verbs/test_light_verbs.py: -------------------------------------------------------------------------------- 1 | from ..helpers import IFPTestCase 2 | 3 | from intficpy.things import LightSource, Thing 4 | 5 | 6 | class TestLightVerb(IFPTestCase): 7 | def setUp(self): 8 | super().setUp() 9 | self.source = LightSource(self.game, "lamp") 10 | self.source.moveTo(self.me) 11 | self.start_room.dark = True 12 | self.start_room.desc = "A spooky forest" 13 | 14 | def test_lighting_source_allows_player_to_see(self): 15 | self.game.turnMain("look") 16 | self.assertNotIn(self.start_room.desc, self.app.print_stack) 17 | self.assertIn("dark", self.app.print_stack.pop()) 18 | self.game.turnMain("light lamp") 19 | self.assertIn("You light the lamp. ", self.app.print_stack) 20 | self.game.turnMain("look") 21 | self.assertIn(self.start_room.desc, self.app.print_stack) 22 | 23 | def test_player_cannot_light_source_if_player_lighting_disabled(self): 24 | self.source.player_can_light = False 25 | self.game.turnMain("look") 26 | self.assertNotIn(self.start_room.desc, self.app.print_stack) 27 | self.assertIn("dark", self.app.print_stack.pop()) 28 | self.game.turnMain("light lamp") 29 | self.assertIn(self.source.cannot_light_msg, self.app.print_stack) 30 | self.game.turnMain("look") 31 | self.assertNotIn(self.start_room.desc, self.app.print_stack) 32 | 33 | def test_player_cannot_light_non_light_source(self): 34 | item = Thing(self.game, "ball") 35 | item.moveTo(self.me) 36 | self.game.turnMain("look") 37 | self.assertNotIn(self.start_room.desc, self.app.print_stack) 38 | self.assertIn("dark", self.app.print_stack.pop()) 39 | self.game.turnMain("light ball") 40 | self.assertIn("not a light source", self.app.print_stack.pop()) 41 | self.game.turnMain("look") 42 | self.assertNotIn(self.start_room.desc, self.app.print_stack) 43 | 44 | 45 | class TestExtinguishVerb(IFPTestCase): 46 | def setUp(self): 47 | super().setUp() 48 | self.source = LightSource(self.game, "lamp") 49 | self.source.moveTo(self.me) 50 | self.source.light(self.game) 51 | self.start_room.dark = True 52 | self.start_room.desc = "A spooky forest" 53 | 54 | def test_extinguishing_source_removes_light(self): 55 | self.game.turnMain("look") 56 | self.assertIn(self.start_room.desc, self.app.print_stack) 57 | self.game.turnMain("put out lamp") 58 | self.assertIn(self.source.extinguish_msg, self.app.print_stack) 59 | self.app.print_stack = [] 60 | self.game.turnMain("look") 61 | self.assertIn("dark", self.app.print_stack.pop()) 62 | self.assertNotIn(self.start_room.desc, self.app.print_stack) 63 | 64 | def test_player_cannot_light_source_if_player_extinguishing_disabled(self): 65 | self.source.player_can_extinguish = False 66 | self.game.turnMain("look") 67 | self.assertIn(self.start_room.desc, self.app.print_stack) 68 | self.game.turnMain("put out lamp") 69 | self.assertIn(self.source.cannot_extinguish_msg, self.app.print_stack) 70 | self.app.print_stack = [] 71 | self.game.turnMain("look") 72 | self.assertIn(self.start_room.desc, self.app.print_stack) 73 | 74 | def test_player_cannot_extinguish_non_light_source(self): 75 | item = Thing(self.game, "ball") 76 | item.moveTo(self.me) 77 | self.game.turnMain("put out ball") 78 | self.assertIn("not a light source", self.app.print_stack.pop()) 79 | -------------------------------------------------------------------------------- /tests/verbs/test_look.py: -------------------------------------------------------------------------------- 1 | from ..helpers import IFPTestCase 2 | 3 | from intficpy.actor import Actor 4 | from intficpy.room import Room 5 | from intficpy.thing_base import Thing 6 | from intficpy.things import ( 7 | Surface, 8 | Container, 9 | UnderSpace, 10 | Readable, 11 | Book, 12 | Transparent, 13 | ) 14 | 15 | 16 | class TestLookVerbs(IFPTestCase): 17 | def test_look(self): 18 | room = Room(self.game, "Strange Room", "It's strange in here. ") 19 | item = Thing(self.game, "hullabaloo") 20 | item.describeThing("All around is a hullabaloo. ") 21 | room.addThing(item) 22 | 23 | self.me.location.removeThing(self.me) 24 | room.addThing(self.me) 25 | 26 | self.game.turnMain("look") 27 | look_desc = self.app.print_stack.pop() 28 | look_title = self.app.print_stack.pop() 29 | 30 | self.assertIn(room.name, look_title, f"Room title printed incorrectly") 31 | self.assertIn(room.desc, look_desc, "Room description not printed by look") 32 | self.assertIn(item.desc, look_desc, "Item description not printed by look") 33 | 34 | def test_examine(self): 35 | item = Thing(self.game, "widget") 36 | item.xdescribeThing("It's a shiny little widget. ") 37 | item.moveTo(self.start_room) 38 | 39 | self.game.turnMain("x widget") 40 | 41 | examine_desc = self.app.print_stack.pop() 42 | 43 | self.assertEqual( 44 | examine_desc, 45 | item.xdesc, 46 | f"Examine desc printed incorrectly. Expecting {item.xdesc}, got {examine_desc}", 47 | ) 48 | 49 | def test_look_in_container(self): 50 | parent = Container(self.game, "shoebox") 51 | child = Thing(self.game, "penny") 52 | self.assertNotIn(child.ix, self.me.knows_about) 53 | parent.addThing(child) 54 | parent.moveTo(self.start_room) 55 | 56 | self.game.turnMain("look in shoebox") 57 | 58 | look_in_desc = self.app.print_stack.pop() 59 | 60 | self.assertEqual( 61 | look_in_desc, 62 | parent.contains_desc, 63 | f"Contains desc printed incorrectly. Expected {parent.contains_desc} got " 64 | f"{look_in_desc}", 65 | ) 66 | self.assertIn(child.ix, self.me.knows_about) 67 | 68 | def test_look_in_closed_container_implies_open_first(self): 69 | parent = Container(self.game, "shoebox") 70 | parent.giveLid() 71 | parent.is_open = False 72 | child = Thing(self.game, "penny") 73 | parent.addThing(child) 74 | parent.moveTo(self.start_room) 75 | 76 | self.game.turnMain("look in shoebox") 77 | 78 | self.assertIn("You open the shoebox", self.app.print_stack.pop(-2)) 79 | 80 | def test_look_in_non_container(self): 81 | parent = Thing(self.game, "cube") 82 | parent.moveTo(self.start_room) 83 | 84 | self.game.turnMain("look in cube") 85 | 86 | self.assertIn("cannot look inside", self.app.print_stack.pop()) 87 | 88 | def test_look_under(self): 89 | parent = UnderSpace(self.game, "table") 90 | child = Thing(self.game, "penny") 91 | self.assertNotIn(child.ix, self.me.knows_about) 92 | parent.addThing(child) 93 | parent.moveTo(self.start_room) 94 | 95 | self.game.turnMain("look under table") 96 | 97 | look_under_desc = self.app.print_stack.pop() 98 | 99 | self.assertEqual( 100 | look_under_desc, 101 | parent.contains_desc, 102 | f"Contains desc printed incorrectly. Expected {parent.contains_desc} got " 103 | f"{look_under_desc}", 104 | ) 105 | self.assertIn(child.ix, self.me.knows_about) 106 | 107 | def test_look_under_empty_underspace(self): 108 | parent = UnderSpace(self.game, "table") 109 | parent.moveTo(self.start_room) 110 | 111 | self.game.turnMain("look under table") 112 | 113 | self.assertIn("There is nothing under the table. ", self.app.print_stack) 114 | 115 | def test_look_under_non_underspace_inv_item(self): 116 | child = Thing(self.game, "penny") 117 | child.invItem = True 118 | child.moveTo(self.start_room) 119 | 120 | self.game.turnMain("look under penny") 121 | 122 | self.assertIn("You take the penny. ", self.app.print_stack) 123 | self.assertIn("You find nothing underneath. ", self.app.print_stack) 124 | 125 | def test_look_under_non_underspace_non_inv_item(self): 126 | child = Thing(self.game, "mountain") 127 | child.invItem = False 128 | child.moveTo(self.start_room) 129 | 130 | self.game.turnMain("look under mountain") 131 | 132 | self.assertIn( 133 | "There's no reason to look under the mountain. ", self.app.print_stack 134 | ) 135 | 136 | def test_read(self): 137 | item = Readable(self.game, "note", "I'm sorry, but I have to do this. ") 138 | item.moveTo(self.start_room) 139 | 140 | self.game.turnMain("read note") 141 | 142 | read_desc = self.app.print_stack.pop() 143 | 144 | self.assertEqual( 145 | read_desc, 146 | item.read_desc, 147 | f"Item text printed incorrectly. Expecting {item.read_desc}, got {read_desc}", 148 | ) 149 | 150 | def test_read_book_starting_from_closed(self): 151 | item = Book(self.game, "note", "I'm sorry, but I have to do this. ") 152 | item.moveTo(self.start_room) 153 | 154 | self.game.turnMain("read note") 155 | 156 | self.assertIn("You open the note. ", self.app.print_stack) 157 | self.assertIn(item.read_desc, self.app.print_stack) 158 | 159 | def test_read_non_readable(self): 160 | item = Surface(self.game, "desk") 161 | item.moveTo(self.start_room) 162 | 163 | self.game.turnMain("read desk") 164 | 165 | self.assertIn("There's nothing written there. ", self.app.print_stack) 166 | 167 | 168 | class TestLookThrough(IFPTestCase): 169 | def test_look_through_transparent_item(self): 170 | item = Transparent(self.game, "glass") 171 | item.moveTo(self.start_room) 172 | self.game.turnMain("look through glass") 173 | self.assertIn("nothing in particular", self.app.print_stack.pop()) 174 | 175 | def test_look_through_non_transparent_item(self): 176 | item = Thing(self.game, "wood") 177 | item.moveTo(self.start_room) 178 | self.game.turnMain("look through wood") 179 | self.assertIn("cannot look through", self.app.print_stack.pop()) 180 | 181 | def test_look_through_person(self): 182 | item = Actor(self.game, "dude") 183 | item.moveTo(self.start_room) 184 | self.game.turnMain("look through dude") 185 | self.assertIn("cannot look through the dude", self.app.print_stack.pop()) 186 | -------------------------------------------------------------------------------- /tests/verbs/test_nested_player.py: -------------------------------------------------------------------------------- 1 | from ..helpers import IFPTestCase 2 | 3 | from intficpy.thing_base import Thing 4 | from intficpy.things import ( 5 | Surface, 6 | Container, 7 | ) 8 | 9 | 10 | class TestPlayerGetOn(IFPTestCase): 11 | def setUp(self): 12 | super().setUp() 13 | self.surface = Surface(self.game, "bench") 14 | self.start_room.addThing(self.surface) 15 | 16 | def test_climb_on_cannot_sit_stand_lie(self): 17 | FAILURE_MSG = f"You cannot climb on {self.surface.lowNameArticle(True)}. " 18 | 19 | self.assertIs( 20 | self.me.location, self.start_room, "Player needs to start in start_room" 21 | ) 22 | self.game.turnMain("climb on bench") 23 | self.assertIs(self.me.location, self.start_room) 24 | 25 | msg = self.app.print_stack.pop() 26 | self.assertEqual(msg, FAILURE_MSG) 27 | 28 | def test_climb_on_can_lie(self): 29 | SUCCESS_MSG = f"You lie on {self.surface.lowNameArticle(True)}. " 30 | 31 | self.surface.can_contain_lying_player = True 32 | 33 | self.assertIs( 34 | self.me.location, self.start_room, "Player needs to start in start_room" 35 | ) 36 | self.game.turnMain("climb on bench") 37 | self.assertIs(self.me.location, self.surface) 38 | 39 | msg = self.app.print_stack.pop() 40 | self.assertEqual(msg, SUCCESS_MSG) 41 | 42 | def test_climb_on_can_sit(self): 43 | SUCCESS_MSG = f"You sit on {self.surface.lowNameArticle(True)}. " 44 | 45 | self.surface.can_contain_sitting_player = True 46 | 47 | self.assertIs( 48 | self.me.location, self.start_room, "Player needs to start in start_room" 49 | ) 50 | self.game.turnMain("climb on bench") 51 | self.assertIs(self.me.location, self.surface) 52 | 53 | msg = self.app.print_stack.pop() 54 | self.assertEqual(msg, SUCCESS_MSG) 55 | 56 | def test_climb_on_can_stand(self): 57 | SUCCESS_MSG = f"You stand on {self.surface.lowNameArticle(True)}. " 58 | 59 | self.surface.can_contain_standing_player = True 60 | 61 | self.assertIs( 62 | self.me.location, self.start_room, "Player needs to start in start_room" 63 | ) 64 | self.game.turnMain("climb on bench") 65 | self.assertIs(self.me.location, self.surface) 66 | 67 | msg = self.app.print_stack.pop() 68 | self.assertEqual(msg, SUCCESS_MSG) 69 | 70 | 71 | class TestPlayerGetOff(IFPTestCase): 72 | def setUp(self): 73 | super().setUp() 74 | self.surface = Surface(self.game, "bench") 75 | self.surface.can_contain_standing_player = True 76 | self.start_room.addThing(self.surface) 77 | self.game.turnMain("climb on bench") 78 | self.assertIs(self.me.location, self.surface) 79 | 80 | def test_climb_down_from(self): 81 | SUCCESS_MSG = f"You climb down from {self.surface.lowNameArticle(True)}. " 82 | 83 | self.game.turnMain("climb down from bench") 84 | self.assertIs(self.me.location, self.start_room) 85 | 86 | msg = self.app.print_stack.pop() 87 | self.assertEqual(msg, SUCCESS_MSG) 88 | 89 | def test_climb_down(self): 90 | SUCCESS_MSG = f"You climb down from {self.surface.lowNameArticle(True)}. " 91 | 92 | self.game.turnMain("climb down") 93 | self.assertIs(self.me.location, self.start_room) 94 | 95 | msg = self.app.print_stack.pop() 96 | self.assertEqual(msg, SUCCESS_MSG) 97 | 98 | 99 | class TestPlayerGetIn(IFPTestCase): 100 | def setUp(self): 101 | super().setUp() 102 | self.container = Container(self.game, "box") 103 | self.start_room.addThing(self.container) 104 | 105 | def test_climb_in_cannot_sit_stand_lie(self): 106 | FAILURE_MSG = f"You cannot climb into {self.container.lowNameArticle(True)}. " 107 | 108 | self.assertIs( 109 | self.me.location, self.start_room, "Player needs to start in start_room" 110 | ) 111 | self.game.turnMain("climb in box") 112 | self.assertIs(self.me.location, self.start_room) 113 | 114 | msg = self.app.print_stack.pop() 115 | self.assertEqual(msg, FAILURE_MSG) 116 | 117 | def test_climb_in_can_lie(self): 118 | SUCCESS_MSG = f"You lie in {self.container.lowNameArticle(True)}. " 119 | 120 | self.container.can_contain_lying_player = True 121 | 122 | self.assertIs( 123 | self.me.location, self.start_room, "Player needs to start in start_room" 124 | ) 125 | self.game.turnMain("climb in box") 126 | self.assertIs(self.me.location, self.container) 127 | 128 | msg = self.app.print_stack.pop() 129 | self.assertEqual(msg, SUCCESS_MSG) 130 | 131 | def test_climb_in_can_sit(self): 132 | SUCCESS_MSG = f"You sit in {self.container.lowNameArticle(True)}. " 133 | 134 | self.container.can_contain_sitting_player = True 135 | 136 | self.assertIs( 137 | self.me.location, self.start_room, "Player needs to start in start_room" 138 | ) 139 | self.game.turnMain("climb in box") 140 | self.assertIs(self.me.location, self.container) 141 | 142 | msg = self.app.print_stack.pop() 143 | self.assertEqual(msg, SUCCESS_MSG) 144 | 145 | def test_climb_in_can_stand(self): 146 | SUCCESS_MSG = f"You stand in {self.container.lowNameArticle(True)}. " 147 | 148 | self.container.can_contain_standing_player = True 149 | 150 | self.assertIs( 151 | self.me.location, self.start_room, "Player needs to start in start_room" 152 | ) 153 | self.game.turnMain("climb in box") 154 | self.assertIs(self.me.location, self.container) 155 | 156 | msg = self.app.print_stack.pop() 157 | self.assertEqual(msg, SUCCESS_MSG) 158 | 159 | 160 | class TestPlayerGetInOpenLid(IFPTestCase): 161 | def setUp(self): 162 | super().setUp() 163 | self.container = Container(self.game, "box") 164 | self.container.has_lid = True 165 | self.container.is_open = True 166 | self.start_room.addThing(self.container) 167 | 168 | def test_climb_in_can_lie(self): 169 | SUCCESS_MSG = f"You lie in {self.container.lowNameArticle(True)}. " 170 | 171 | self.container.can_contain_lying_player = True 172 | 173 | self.assertIs( 174 | self.me.location, self.start_room, "Player needs to start in start_room" 175 | ) 176 | self.game.turnMain("climb in box") 177 | self.assertIs(self.me.location, self.container) 178 | 179 | msg = self.app.print_stack.pop() 180 | self.assertEqual(msg, SUCCESS_MSG) 181 | 182 | def test_climb_in_can_sit(self): 183 | SUCCESS_MSG = f"You sit in {self.container.lowNameArticle(True)}. " 184 | 185 | self.container.can_contain_sitting_player = True 186 | 187 | self.assertIs( 188 | self.me.location, self.start_room, "Player needs to start in start_room" 189 | ) 190 | self.game.turnMain("climb in box") 191 | self.assertIs(self.me.location, self.container) 192 | 193 | msg = self.app.print_stack.pop() 194 | self.assertEqual(msg, SUCCESS_MSG) 195 | 196 | def test_climb_in_can_stand(self): 197 | SUCCESS_MSG = f"You stand in {self.container.lowNameArticle(True)}. " 198 | 199 | self.container.can_contain_standing_player = True 200 | 201 | self.assertIs( 202 | self.me.location, self.start_room, "Player needs to start in start_room" 203 | ) 204 | self.game.turnMain("climb in box") 205 | self.assertIs(self.me.location, self.container) 206 | 207 | msg = self.app.print_stack.pop() 208 | self.assertEqual(msg, SUCCESS_MSG) 209 | 210 | 211 | class TestPlayerGetInClosedLid(IFPTestCase): 212 | def setUp(self): 213 | super().setUp() 214 | self.container = Container(self.game, "box") 215 | self.container.has_lid = True 216 | self.container.is_open = False 217 | self.start_room.addThing(self.container) 218 | 219 | def test_climb_in_can_lie(self): 220 | FAILURE_MSG = ( 221 | f"You cannot climb into {self.container.lowNameArticle(True)}, " 222 | "since it is closed. " 223 | ) 224 | 225 | self.container.can_contain_lying_player = True 226 | 227 | self.assertIs( 228 | self.me.location, self.start_room, "Player needs to start in start_room" 229 | ) 230 | self.game.turnMain("climb in box") 231 | self.assertIs(self.me.location, self.start_room) 232 | 233 | msg = self.app.print_stack.pop() 234 | self.assertEqual(msg, FAILURE_MSG) 235 | 236 | def test_climb_in_can_sit(self): 237 | FAILURE_MSG = ( 238 | f"You cannot climb into {self.container.lowNameArticle(True)}, " 239 | "since it is closed. " 240 | ) 241 | 242 | self.container.can_contain_sitting_player = True 243 | 244 | self.assertIs( 245 | self.me.location, self.start_room, "Player needs to start in start_room" 246 | ) 247 | self.game.turnMain("climb in box") 248 | self.assertIs(self.me.location, self.start_room) 249 | 250 | msg = self.app.print_stack.pop() 251 | self.assertEqual(msg, FAILURE_MSG) 252 | 253 | def test_climb_in_can_stand(self): 254 | FAILURE_MSG = ( 255 | f"You cannot climb into {self.container.lowNameArticle(True)}, " 256 | "since it is closed. " 257 | ) 258 | 259 | self.container.can_contain_standing_player = True 260 | 261 | self.assertIs( 262 | self.me.location, self.start_room, "Player needs to start in start_room" 263 | ) 264 | self.game.turnMain("climb in box") 265 | self.assertIs(self.me.location, self.start_room) 266 | 267 | msg = self.app.print_stack.pop() 268 | self.assertEqual(msg, FAILURE_MSG) 269 | 270 | 271 | class TestPlayerGetOut(IFPTestCase): 272 | def setUp(self): 273 | super().setUp() 274 | self.container = Container(self.game, "box") 275 | self.container.can_contain_standing_player = True 276 | self.start_room.addThing(self.container) 277 | self.game.turnMain("climb in box") 278 | self.assertIs(self.me.location, self.container) 279 | 280 | def test_climb_out_of(self): 281 | SUCCESS_MSG = f"You climb out of {self.container.lowNameArticle(True)}. " 282 | 283 | self.game.turnMain("climb out of box") 284 | self.assertIs(self.me.location, self.start_room) 285 | 286 | msg = self.app.print_stack.pop() 287 | self.assertEqual(msg, SUCCESS_MSG) 288 | 289 | def test_climb_out(self): 290 | SUCCESS_MSG = f"You climb out of {self.container.lowNameArticle(True)}. " 291 | 292 | self.game.turnMain("climb out") 293 | self.assertIs(self.me.location, self.start_room) 294 | 295 | msg = self.app.print_stack.pop() 296 | self.assertEqual(msg, SUCCESS_MSG) 297 | -------------------------------------------------------------------------------- /tests/verbs/test_open_close_lock.py: -------------------------------------------------------------------------------- 1 | from ..helpers import IFPTestCase 2 | 3 | from intficpy.things import ( 4 | Container, 5 | Lock, 6 | Key, 7 | ) 8 | from intficpy.travel import DoorConnector 9 | from intficpy.room import Room 10 | 11 | 12 | class TestDoorVerbs(IFPTestCase): 13 | def setUp(self): 14 | super().setUp() 15 | self.room2 = Room(self.game, "A hot room", "This room is uncomfortably hot. ") 16 | self.door = DoorConnector(self.game, self.start_room, "se", self.room2, "nw") 17 | self.key = Key(self.game, "key") 18 | self.me.addThing(self.key) 19 | self.lock = Lock(self.game, False, self.key) 20 | self.lock.is_locked = False 21 | self.door.setLock(self.lock) 22 | 23 | def test_open_door(self): 24 | self.assertFalse( 25 | self.door.entrance_a.is_open, 26 | "This test needs the door to be initially closed", 27 | ) 28 | self.assertFalse( 29 | self.door.entrance_a.lock_obj.is_locked, 30 | "This test needs the door to be initially unlocked", 31 | ) 32 | 33 | self.game.turnMain("open door") 34 | 35 | self.assertTrue( 36 | self.door.entrance_a.is_open, 37 | "Performed open verb on unlocked door, but door is closed. " 38 | f"Msg: {self.app.print_stack[-1]}", 39 | ) 40 | 41 | def test_close_door(self): 42 | self.door.entrance_a.makeOpen() 43 | self.assertTrue( 44 | self.door.entrance_a.is_open, 45 | "This test needs the door to be initially open", 46 | ) 47 | 48 | self.game.turnMain("close door") 49 | 50 | self.assertFalse( 51 | self.door.entrance_a.is_open, 52 | "Performed close verb on open door, but door is open. " 53 | f"Msg: {self.app.print_stack[-1]}", 54 | ) 55 | 56 | def test_lock_door(self): 57 | self.lock.is_locked = False 58 | self.assertIn(self.key.ix, self.me.contains) 59 | self.assertIn(self.key, self.me.contains[self.key.ix]) 60 | 61 | self.game.turnMain("lock door") 62 | 63 | self.assertTrue( 64 | self.lock.is_locked, 65 | "Performed lock verb with key in inv, but lock is unlocked. " 66 | f"Msg: {self.app.print_stack[-1]}", 67 | ) 68 | 69 | def test_unlock_door(self): 70 | self.lock.is_locked = True 71 | self.assertIn(self.key.ix, self.me.contains) 72 | self.assertIn(self.key, self.me.contains[self.key.ix]) 73 | 74 | self.game.turnMain("unlock door") 75 | 76 | self.assertFalse( 77 | self.lock.is_locked, 78 | "Performed unlock verb with key in inv, but lock is locked. " 79 | f"Msg: {self.app.print_stack[-1]}", 80 | ) 81 | 82 | def test_lock_door_with(self): 83 | self.lock.is_locked = False 84 | self.assertIn(self.key.ix, self.me.contains) 85 | self.assertIn(self.key, self.me.contains[self.key.ix]) 86 | 87 | self.game.turnMain("lock door with key") 88 | 89 | self.assertTrue( 90 | self.lock.is_locked, 91 | "Performed lock with verb with key, but lock is unlocked. " 92 | f"Msg: {self.app.print_stack[-1]}", 93 | ) 94 | 95 | def test_unlock_door_with(self): 96 | self.lock.is_locked = True 97 | self.assertIn(self.key.ix, self.me.contains) 98 | self.assertIn(self.key, self.me.contains[self.key.ix]) 99 | 100 | self.game.turnMain("unlock door with key") 101 | 102 | self.assertFalse( 103 | self.lock.is_locked, 104 | "Performed unlock verb with key, but lock is locked. " 105 | f"Msg: {self.app.print_stack[-1]}", 106 | ) 107 | 108 | def test_open_locked_door(self): 109 | self.lock.is_locked = True 110 | self.assertFalse( 111 | self.door.entrance_a.is_open, 112 | "This test needs the door to be initially closed", 113 | ) 114 | self.assertTrue( 115 | self.door.entrance_a.lock_obj.is_locked, 116 | "This test needs the door to be initially locked", 117 | ) 118 | 119 | self.game.turnMain("open door") 120 | 121 | self.assertFalse( 122 | self.door.entrance_a.is_open, 123 | "Performed open verb on locked door, but door is open. " 124 | f"Msg: {self.app.print_stack[-1]}", 125 | ) 126 | 127 | 128 | class TestLidVerbs(IFPTestCase): 129 | def setUp(self): 130 | super().setUp() 131 | self.container = Container(self.game, "chest") 132 | self.container.has_lid = True 133 | self.container.is_open = False 134 | self.key = Key(self.game, "key") 135 | self.me.addThing(self.key) 136 | self.lock = Lock(self.game, False, self.key) 137 | self.lock.is_locked = False 138 | self.container.setLock(self.lock) 139 | self.container.moveTo(self.start_room) 140 | 141 | def test_open_container(self): 142 | self.assertFalse( 143 | self.container.is_open, 144 | "This test needs the container to be initially closed", 145 | ) 146 | self.assertFalse( 147 | self.container.lock_obj.is_locked, 148 | "This test needs the container to be initially unlocked", 149 | ) 150 | 151 | self.game.turnMain("open chest") 152 | 153 | self.assertTrue( 154 | self.container.is_open, 155 | "Performed open verb on unlocked container, but lid is closed. " 156 | f"Msg: {self.app.print_stack[-1]}", 157 | ) 158 | 159 | def test_close_container(self): 160 | self.container.is_open = True 161 | self.assertTrue( 162 | self.container.is_open, "This test needs the container to be initially open" 163 | ) 164 | 165 | self.game.turnMain("close chest") 166 | 167 | self.assertFalse( 168 | self.container.is_open, 169 | "Performed close verb on open container, but lid is open. " 170 | f"Msg: {self.app.print_stack[-1]}", 171 | ) 172 | 173 | def test_open_locked_container(self): 174 | self.lock.is_locked = True 175 | self.assertFalse( 176 | self.container.is_open, 177 | "This test needs the container to be initially closed", 178 | ) 179 | self.assertTrue( 180 | self.container.lock_obj.is_locked, 181 | "This test needs the container to be initially locked", 182 | ) 183 | 184 | self.game.turnMain("open chest") 185 | 186 | self.assertFalse( 187 | self.container.is_open, 188 | "Performed open verb on locked container, but lid is open. " 189 | f"Msg: {self.app.print_stack[-1]}", 190 | ) 191 | -------------------------------------------------------------------------------- /tests/verbs/test_position_verbs.py: -------------------------------------------------------------------------------- 1 | from tests.helpers import IFPTestCase 2 | 3 | from intficpy.things import Container 4 | 5 | 6 | LYING = "lying" 7 | SITTING = "sitting" 8 | 9 | 10 | class TestLieDownVerb(IFPTestCase): 11 | def test_cannot_lie_down_if_already_lying_down(self): 12 | self.me.position = LYING 13 | self.game.turnMain("lie down") 14 | response = self.app.print_stack[-1] 15 | self.assertIn("already lying down", response) 16 | 17 | def test_must_get_out_if_nested_location_does_not_allow_lying(self): 18 | inner_loc = Container(self.game, "box") 19 | inner_loc.moveTo(self.start_room) 20 | self.me.moveTo(inner_loc) 21 | self.assertFalse(inner_loc.can_contain_sitting_player) 22 | 23 | self.game.turnMain("lie down") 24 | first_getting_out = self.app.print_stack[-2] 25 | self.assertIn("climb out", first_getting_out) 26 | self.assertEqual(self.me.location, inner_loc.location) 27 | self.assertEqual(self.me.position, LYING) 28 | 29 | def test_deeply_nested_player(self): 30 | locs = [ 31 | Container(self.game, "box0"), 32 | Container(self.game, "box1"), 33 | Container(self.game, "box2"), 34 | Container(self.game, "box3"), 35 | ] 36 | locs[0].moveTo(self.start_room) 37 | locs[1].moveTo(locs[0]) 38 | locs[2].moveTo(locs[1]) 39 | locs[3].moveTo(locs[2]) 40 | self.me.moveTo(locs[3]) 41 | 42 | for loc in locs: 43 | self.assertFalse(loc.can_contain_sitting_player) 44 | 45 | self.game.turnMain("lie down") 46 | action = self.app.print_stack.pop() 47 | self.assertIn("You lie down", action) 48 | 49 | for ix in range(len(locs)): 50 | implied = self.app.print_stack.pop() 51 | self.assertIn("climb out", implied, ix) 52 | 53 | self.assertEqual(self.me.location, self.start_room) 54 | self.assertEqual(self.me.position, LYING) 55 | 56 | def test_can_lie_down(self): 57 | self.assertNotEqual(self.me.position, LYING) 58 | self.game.turnMain("lie down") 59 | self.assertEqual(self.me.position, LYING) 60 | 61 | 62 | class TestSitDownVerb(IFPTestCase): 63 | def test_cannot_sit_down_if_already_lying_down(self): 64 | self.me.position = SITTING 65 | self.game.turnMain("sit down") 66 | response = self.app.print_stack[-1] 67 | self.assertIn("already sitting", response) 68 | 69 | def test_must_get_out_if_nested_location_does_not_allow_sitting(self): 70 | inner_loc = Container(self.game, "box") 71 | inner_loc.moveTo(self.start_room) 72 | self.me.moveTo(inner_loc) 73 | self.assertFalse(inner_loc.can_contain_sitting_player) 74 | 75 | self.game.turnMain("sit down") 76 | first_getting_out = self.app.print_stack[-2] 77 | self.assertIn("climb out", first_getting_out) 78 | self.assertEqual(self.me.location, inner_loc.location) 79 | self.assertEqual(self.me.position, SITTING) 80 | 81 | def test_deeply_nested_player(self): 82 | locs = [ 83 | Container(self.game, "box0"), 84 | Container(self.game, "box1"), 85 | Container(self.game, "box2"), 86 | Container(self.game, "box3"), 87 | ] 88 | locs[0].moveTo(self.start_room) 89 | locs[1].moveTo(locs[0]) 90 | locs[2].moveTo(locs[1]) 91 | locs[3].moveTo(locs[2]) 92 | self.me.moveTo(locs[3]) 93 | 94 | for loc in locs: 95 | self.assertFalse(loc.can_contain_sitting_player) 96 | 97 | self.game.turnMain("sit down") 98 | action = self.app.print_stack.pop() 99 | 100 | self.assertIn("You sit down", action) 101 | 102 | for ix in range(len(locs)): 103 | implied = self.app.print_stack.pop() 104 | self.assertIn("climb out", implied, ix) 105 | 106 | self.assertEqual(self.me.location, self.start_room) 107 | self.assertEqual(self.me.position, SITTING) 108 | 109 | def test_can_sit_down(self): 110 | self.assertNotEqual(self.me.position, SITTING) 111 | self.game.turnMain("sit down") 112 | self.assertEqual(self.me.position, SITTING) 113 | -------------------------------------------------------------------------------- /tests/verbs/test_set_verbs.py: -------------------------------------------------------------------------------- 1 | from ..helpers import IFPTestCase 2 | 3 | from intficpy.things import ( 4 | Thing, 5 | Surface, 6 | Container, 7 | UnderSpace, 8 | Liquid, 9 | ) 10 | 11 | 12 | class TestAddItemToItself(IFPTestCase): 13 | def test_set_surface_on_itself(self): 14 | item = Surface(self.game, "thing") 15 | item.moveTo(self.start_room) 16 | self.game.turnMain("set thing on thing") 17 | self.assertIn("You cannot", self.app.print_stack.pop()) 18 | self.assertFalse(item.containsItem(item)) 19 | 20 | def test_set_container_in_itself(self): 21 | item = Container(self.game, "thing") 22 | item.moveTo(self.start_room) 23 | self.game.turnMain("set thing in thing") 24 | self.assertIn("You cannot", self.app.print_stack.pop()) 25 | self.assertFalse(item.containsItem(item)) 26 | 27 | def test_set_underspace_under_itself(self): 28 | item = UnderSpace(self.game, "thing") 29 | item.moveTo(self.start_room) 30 | self.game.turnMain("set thing under thing") 31 | self.assertIn("You cannot", self.app.print_stack.pop()) 32 | self.assertFalse(item.containsItem(item)) 33 | 34 | 35 | class TestSetVerbs(IFPTestCase): 36 | def test_set_in_adds_item(self): 37 | item = Thing(self.game, self._get_unique_noun()) 38 | item.invItem = True 39 | self.me.addThing(item) 40 | container = Container(self.game, self._get_unique_noun()) 41 | self.start_room.addThing(container) 42 | 43 | self.assertNotIn(item.ix, container.contains) 44 | 45 | self.game.turnMain(f"set {item.verbose_name} in {container.verbose_name}") 46 | 47 | self.assertIn(item.ix, container.contains) 48 | self.assertIn(item, container.contains[item.ix]) 49 | 50 | def test_set_composite_child_in_gives_attached_message(self): 51 | parent = Thing(self.game, "thing") 52 | parent.moveTo(self.me) 53 | item = Thing(self.game, "handle") 54 | parent.addComposite(item) 55 | container = Container(self.game, "place") 56 | self.start_room.addThing(container) 57 | 58 | self.game.turnMain(f"set handle in place") 59 | 60 | self.assertIn("is attached to", self.app.print_stack.pop()) 61 | self.assertIs(item.location, self.me) 62 | 63 | def test_set_in_gives_too_big_message_if_item_too_big(self): 64 | item = Thing(self.game, "giant") 65 | item.size = 100000 66 | self.me.addThing(item) 67 | place = Container(self.game, "place") 68 | self.start_room.addThing(place) 69 | 70 | self.game.turnMain(f"set giant in place") 71 | 72 | self.assertIn("too big", self.app.print_stack.pop()) 73 | self.assertFalse(place.containsItem(item)) 74 | 75 | def test_set_in_closed_container_implies_open_it_first(self): 76 | item = Thing(self.game, "item") 77 | self.me.addThing(item) 78 | place = Container(self.game, "place") 79 | place.giveLid() 80 | place.is_open = False 81 | self.start_room.addThing(place) 82 | 83 | self.game.turnMain(f"set item in place") 84 | 85 | self.assertIn("You set the item in the place", self.app.print_stack.pop()) 86 | self.assertIn("You open the place", self.app.print_stack.pop()) 87 | self.assertIn("(First trying to open", self.app.print_stack.pop()) 88 | self.assertTrue(place.is_open) 89 | self.assertTrue(place.containsItem(item)) 90 | 91 | def test_cannot_set_item_in_if_container_already_contains_liquid(self): 92 | item = Thing(self.game, "item") 93 | self.me.addThing(item) 94 | place = Container(self.game, "place") 95 | self.start_room.addThing(place) 96 | liquid = Liquid(self.game, "water", "water") 97 | liquid.moveTo(place) 98 | 99 | self.game.turnMain(f"set item in place") 100 | 101 | self.assertIn("already full of water", self.app.print_stack.pop()) 102 | self.assertFalse(place.containsItem(item)) 103 | 104 | def test_set_on_adds_item(self): 105 | item = Thing(self.game, self._get_unique_noun()) 106 | item.invItem = True 107 | self.me.addThing(item) 108 | surface = Surface(self.game, self._get_unique_noun()) 109 | self.start_room.addThing(surface) 110 | 111 | self.assertNotIn(item.ix, surface.contains) 112 | 113 | self.game.turnMain(f"set {item.verbose_name} on {surface.verbose_name}") 114 | 115 | self.assertIn(item.ix, surface.contains) 116 | self.assertIn(item, surface.contains[item.ix]) 117 | 118 | def test_set_on_floor_add_item_to_outer_room(self): 119 | item = Thing(self.game, "thing") 120 | item.invItem = True 121 | item.moveTo(self.game.me) 122 | 123 | self.assertIsNot(item.location, self.start_room) 124 | 125 | self.game.turnMain(f"set thing on floor") 126 | 127 | self.assertIs(item.location, self.start_room) 128 | 129 | def test_set_composite_child_on_gives_attached_message(self): 130 | parent = Thing(self.game, "thing") 131 | parent.moveTo(self.me) 132 | item = Thing(self.game, "handle") 133 | parent.addComposite(item) 134 | surface = Surface(self.game, "place") 135 | self.start_room.addThing(surface) 136 | 137 | self.game.turnMain(f"set handle on place") 138 | 139 | self.assertIn("is attached to", self.app.print_stack.pop()) 140 | self.assertIs(item.location, self.me) 141 | 142 | def test_set_composite_child_on_floor_gives_attached_message(self): 143 | parent = Thing(self.game, "thing") 144 | parent.moveTo(self.me) 145 | item = Thing(self.game, "handle") 146 | parent.addComposite(item) 147 | 148 | self.assertIsNot(item.location, self.start_room) 149 | 150 | self.game.turnMain(f"set handle on floor") 151 | 152 | self.assertIn("is attached to", self.app.print_stack.pop()) 153 | self.assertIs(item.location, self.me) 154 | 155 | def test_set_under_adds_item(self): 156 | item = Thing(self.game, self._get_unique_noun()) 157 | item.invItem = True 158 | self.me.addThing(item) 159 | underspace = UnderSpace(self.game, self._get_unique_noun()) 160 | self.start_room.addThing(underspace) 161 | 162 | self.assertNotIn(item.ix, underspace.contains) 163 | 164 | self.game.turnMain(f"set {item.verbose_name} under {underspace.verbose_name}") 165 | 166 | self.assertIn(item.ix, underspace.contains) 167 | self.assertIn(item, underspace.contains[item.ix]) 168 | 169 | def test_set_under_gives_too_big_message_if_item_too_big(self): 170 | item = Thing(self.game, "giant") 171 | item.size = 100000 172 | self.me.addThing(item) 173 | place = UnderSpace(self.game, "place") 174 | self.start_room.addThing(place) 175 | 176 | self.game.turnMain(f"set giant under place") 177 | 178 | self.assertIn("too big", self.app.print_stack.pop()) 179 | self.assertFalse(place.containsItem(item)) 180 | 181 | def test_set_composite_child_under_gives_attached_message(self): 182 | parent = Thing(self.game, "thing") 183 | parent.moveTo(self.me) 184 | item = Thing(self.game, "handle") 185 | parent.addComposite(item) 186 | underspace = UnderSpace(self.game, "place") 187 | self.start_room.addThing(underspace) 188 | 189 | self.game.turnMain(f"set handle under place") 190 | 191 | self.assertIn("is attached to", self.app.print_stack.pop()) 192 | self.assertIs(item.location, self.me) 193 | 194 | def test_cannot_set_in_non_container(self): 195 | item = Thing(self.game, self._get_unique_noun()) 196 | item.invItem = True 197 | self.me.addThing(item) 198 | invalid_iobj = Thing(self.game, self._get_unique_noun()) 199 | self.start_room.addThing(invalid_iobj) 200 | 201 | self.assertNotIn(item.ix, invalid_iobj.contains) 202 | 203 | self.game.turnMain(f"set {item.verbose_name} in {invalid_iobj.verbose_name}") 204 | 205 | self.assertNotIn(item.ix, invalid_iobj.contains) 206 | 207 | def test_cannot_set_on_non_surface(self): 208 | item = Thing(self.game, self._get_unique_noun()) 209 | item.invItem = True 210 | self.me.addThing(item) 211 | invalid_iobj = Thing(self.game, self._get_unique_noun()) 212 | self.start_room.addThing(invalid_iobj) 213 | 214 | self.assertNotIn(item.ix, invalid_iobj.contains) 215 | 216 | self.game.turnMain(f"set {item.verbose_name} on {invalid_iobj.verbose_name}") 217 | 218 | self.assertNotIn(item.ix, invalid_iobj.contains) 219 | 220 | def test_cannot_set_under_non_underspace(self): 221 | item = Thing(self.game, self._get_unique_noun()) 222 | item.invItem = True 223 | self.me.addThing(item) 224 | invalid_iobj = Thing(self.game, self._get_unique_noun()) 225 | self.start_room.addThing(invalid_iobj) 226 | 227 | self.assertNotIn(item.ix, invalid_iobj.contains) 228 | 229 | self.game.turnMain(f"set {item.verbose_name} under {invalid_iobj.verbose_name}") 230 | 231 | self.assertNotIn(item.ix, invalid_iobj.contains) 232 | --------------------------------------------------------------------------------