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