From 7f86aafc0581e66fc5598165c322d0e7880359b1 Mon Sep 17 00:00:00 2001 From: Alek Ratzloff Date: Thu, 18 Nov 2021 16:26:16 -0800 Subject: [PATCH] Update example game some, update engine some * Add RevealAction/UnrevealAction for revealing/hiding items in a room * Add a lot of checks for items being revealed when it's attempted to be triggered * Implement TeleportAction (mostly) * For all Check* family of actions, the `yes` and `no` values may be just be a single action instead of an array of actions * Change up how room descriptions and stuff work, mostly so that you can specify multiple lines in an array so you can preserve paragraph breaks when displayed. * Example game has some more content Signed-off-by: Alek Ratzloff --- agame/action.py | 104 +++++++++++++++- agame/game.py | 45 ++++--- agame/item.py | 6 +- agame/room.py | 4 +- agame/trigger.py | 70 ++++++++--- agame/util.py | 2 +- examplegame/__init__.py | 3 +- examplegame/items.py | 72 ----------- examplegame/rooms.py | 258 +++++++++++++++++++++++++++++++++++++++- examplegame/vars.py | 3 +- 10 files changed, 446 insertions(+), 121 deletions(-) delete mode 100644 examplegame/items.py diff --git a/agame/action.py b/agame/action.py index 89fd7da..5a4ca99 100644 --- a/agame/action.py +++ b/agame/action.py @@ -19,6 +19,8 @@ __all__ = ( "Var", "CheckVarAction", "SetVarAction", + "RevealItemAction", + "UnrevealItemAction", ) @@ -79,8 +81,10 @@ class TeleportAction(Action): room_id: str def act(self, game: "Game"): - # TODO - raise NotImplementedError("TODO - implement teleport action") + assert ( + self.room_id in game.database.rooms + ), f"could not find room with id {self.room_id}" + game.room = game.database.rooms[self.room_id] @dataclasses.dataclass @@ -126,6 +130,24 @@ class CheckInvItemsAction(Action): yes: Sequence[Action] no: Sequence[Action] + def __init__( + self, + item_ids: Union[str, Sequence[str]], + yes: Union[Action, Sequence[Action]] = [], + no: Union[Action, Sequence[Action]] = [], + ): + self.item_ids = item_ids + # Put the "yes" action into an array if necessary + if isinstance(yes, Action): + self.yes = [yes] + else: + self.yes = yes + # Put the "no" action into an array if necessary + if isinstance(no, Action): + self.no = [no] + else: + self.no = no + def act(self, game: "Game"): # If the item_ids are just a string, use that as a list. items = [self.item_ids] if isinstance(self.item_ids, str) else self.item_ids @@ -143,10 +165,28 @@ class CheckRoomItemsAction(Action): appropriate action sequence. """ - item_ids: str + item_ids: Union[str, Sequence[str]] yes: Sequence[Action] no: Sequence[Action] + def __init__( + self, + item_ids: Union[str, Sequence[str]], + yes: Union[Action, Sequence[Action]] = [], + no: Union[Action, Sequence[Action]] = [], + ): + self.item_ids = item_ids + # Put the "yes" action into an array if necessary + if isinstance(yes, Action): + self.yes = [yes] + else: + self.yes = yes + # Put the "no" action into an array if necessary + if isinstance(no, Action): + self.no = [no] + else: + self.no = no + def act(self, game: "Game"): items = [self.item_ids] if isinstance(self.item_ids, str) else self.item_ids for item_id in items: @@ -225,10 +265,37 @@ class CheckVarAction(Action): # The action to execute when this is false. no: Sequence[Action] + def __init__( + self, + var_id: str, + compare: Compare = Compare.EQUALS, + against: Any = True, + yes: Union[Action, Sequence[Action]] = [], + no: Union[Action, Sequence[Action]] = [], + ): + self.var_id = var_id + self.compare = compare + self.against = against + # Put the "yes" action into an array if necessary + if isinstance(yes, Action): + self.yes = [yes] + else: + self.yes = yes + # Put the "no" action into an array if necessary + if isinstance(no, Action): + self.no = [no] + else: + self.no = no + def act(self, game: "Game"): + assert self.var_id in game.vars, f"variable '{self.var_id}' does not exist" + val = game.vars.get(self.var_id, None) compare_val = None if isinstance(self.against, Var): + assert ( + self.against.id in game.vars + ), f"variable '{self.against.id}' does not exist" compare_val = game.vars.get(self.against.id, None) else: compare_val = self.against @@ -288,7 +355,38 @@ class SetVarAction(Action): value: Any def act(self, game: "Game"): + assert self.var_id in game.vars, f"variable '{self.var_id}' does not exist" value = ( game.vars.get(self.value.id) if isinstance(self.value, Var) else self.value ) game.vars[self.var_id] = value + + +@dataclasses.dataclass +class RevealItemAction(Action): + """ + Reveal an item in the current room. + """ + + item_id: str + + def act(self, game: "Game"): + assert ( + self.item_id in game.room.items + ), f"item id {self.item_id} does not exist in the current room {game.room.id}" + game.room.items[self.item_id].revealed = True + + +@dataclasses.dataclass +class UnrevealItemAction(Action): + """ + Unreveal an item in the current room. + """ + + item_id: str + + def act(self, game: "Game"): + assert ( + self.item_id in game.room.items + ), f"item id {self.item_id} does not exist in the current room {game.room.id}" + game.room.items[self.item_id].revealed = False diff --git a/agame/game.py b/agame/game.py index 7416706..bbc1529 100644 --- a/agame/game.py +++ b/agame/game.py @@ -57,6 +57,7 @@ class Game: UseTrigger(), PutTrigger(), LookTrigger(), + ReadTrigger(), OpenTrigger(), CloseTrigger(), GoTrigger(), @@ -83,24 +84,40 @@ class Game: def print_room(self): "Prints this room's description." - self.say(self.room.name) - self.say() - self.say(self.room.desc) + self.say(f"(({self.room.name}))") + if isinstance(self.room.desc, str): + self.say(self.room.desc) + else: + assert isinstance( + self.room.desc, Sequence + ), f"room.desc is not a list or a string from room id {room.id}" + self.say(*self.room.desc) # Look at revealed text - for item in self.room.items.values(): - if not item.revealed or item.room_desc is None: + for (index, item) in enumerate(self.room.items.values()): + if not item.revealed or item.room_desc is None or item.room_desc == []: continue - if item.room_desc == "": - # TODO - pluralization, 'a' vs 'an' - self.say(f"You see a (({item.name})).") - else: - self.say(item.room_desc) - def say(self, message: Optional[str] = None): + if index != 0: + # Space this out with blank lines + self.say() + + if isinstance(item.room_desc, str): + self.say(item.room_desc) + else: + assert isinstance(item.room_desc, Sequence) + self.say(*item.room_desc) + + def say(self, *lines: str): "Format, colorize, wrap, and print the message." - message = message or "" - message = textwrap.fill(message) - print(colorize(message)) + if lines: + head = textwrap.fill(lines[0]) + print(colorize(head)) + for line in lines[1:]: + message = textwrap.fill(line) + print() + print(colorize(message)) + else: + print() @property def rooms(self): diff --git a/agame/item.py b/agame/item.py index 8bf002d..2050c0c 100644 --- a/agame/item.py +++ b/agame/item.py @@ -1,5 +1,5 @@ import dataclasses -from typing import Mapping, Optional, Sequence +from typing import Mapping, Optional, Sequence, Union from agame.action import Action @@ -44,7 +44,7 @@ class ItemInst: return self.item.synonyms @property - def room_desc(self) -> Optional[str]: + def room_desc(self) -> Optional[Union[str, Sequence[str]]]: return self.item.room_desc @property @@ -87,7 +87,7 @@ class Item: # # Otherwise, the `room_desc` string will be colorized and printed as # written. - room_desc: Optional[str] = None + room_desc: Optional[Union[str, Sequence[str]]] = None # A list of triggers that a game may use. Since this is just a mapping of # strings to action sequences, only one set of actions is allowed per diff --git a/agame/room.py b/agame/room.py index bd146f1..bd5031a 100644 --- a/agame/room.py +++ b/agame/room.py @@ -11,14 +11,14 @@ __all__ = ("Room",) class Room: id: str name: str - desc: str + desc: Union[str, Sequence[str]] items: MutableMapping[str, ItemInst] def __init__( self, id: str, name: str, - desc: str, + desc: Union[str, Sequence[str]], items: Union[Sequence[ItemInst], MutableMapping[str, ItemInst]], ): self.id = id diff --git a/agame/trigger.py b/agame/trigger.py index 233dfb9..4096785 100644 --- a/agame/trigger.py +++ b/agame/trigger.py @@ -12,12 +12,13 @@ if TYPE_CHECKING: # use [a[n]/the] x [with/on [a[n]/the] y] # put [a[n]/the] x on/in [a[n]/the] y # look [[at] [a[n]/the] x] +# read [a[n]/the] x # open x # close x # go/(go to)/leave/exit x -# give [a[n]/the] x to [a[n]/the] y -# push [a[n]/the] x -# pull [a[n]/the] x +# give [a[n]/the] x to [a[n]/the] y - TODO +# push [a[n]/the] x - TODO +# pull [a[n]/the] x - TODO # (put down)/drop [a[n]/the] x __all__ = ( "Trigger", @@ -25,6 +26,7 @@ __all__ = ( "UseTrigger", "PutTrigger", "LookTrigger", + "ReadTrigger", "OpenTrigger", "CloseTrigger", "GoTrigger", @@ -32,6 +34,7 @@ __all__ = ( "USE", "PUT", "LOOK", + "READ", "OPEN", "CLOSE", "GO", @@ -40,6 +43,7 @@ GET = "get" USE = "use" PUT = "put" LOOK = "look" +READ = "read" OPEN = "open" CLOSE = "close" GO = "go" @@ -73,8 +77,8 @@ class GetTrigger(Trigger): otrigger = match["trigger"].lower().capitalize() game.say(f"{otrigger} what?") return - item = game.room.search_item_name(item_name.lower()) - if item and GET in item.triggers: + item = game.room.search_item_name(item_name) + if item and item.revealed and GET in item.triggers: actions = item.triggers[GET] # if there are any actions, do them. else do the default action game.do_actions(actions) @@ -106,9 +110,9 @@ class UseTrigger(Trigger): # Get the item from inventory or room item = game.room.search_item_name(item_name) or search_item_name( - game.inventory.values(), item_name.lower() + game.inventory.values(), item_name ) - if not item: + if not item or not item.revealed: game.say(f"I'm not sure what you mean by {item_name}.") return @@ -116,9 +120,9 @@ class UseTrigger(Trigger): if target_name: # Get the target from inventory or room target = game.room.search_item_name(target_name) or search_item_name( - game.inventory.values(), target_name.lower() + game.inventory.values(), target_name ) - if not target: + if not target or not target.revealed: game.say(f"I'm not sure what you mean by {target_name}.") # Check if the target can be used on something elif target.id in item.use_actions: @@ -168,14 +172,42 @@ class LookTrigger(Trigger): if not item_name: game.print_room() return - item = game.room.search_item_name(item_name.lower()) - if item and LOOK in item.triggers: + item = game.room.search_item_name(item_name) or search_item_name( + game.inventory.values(), item_name + ) + if item and LOOK in item.triggers and item.revealed: actions = item.triggers[LOOK] game.do_actions(actions) else: game.say("Can't see that.") +class ReadTrigger(Trigger): + @staticmethod + def pattern() -> Pattern: + return re.compile( + r""" + (?Pread) + ([ ]+((an?|the)[ ]+)?(?P.+?))? + """, + re.IGNORECASE | re.VERBOSE, + ) + + def trigger(self, game: "Game", match: Match): + item_name = match["item"] + if not item_name: + game.print_room() + return + item = game.room.search_item_name(item_name) or search_item_name( + game.inventory.values(), item_name + ) + if item and READ in item.triggers and item.revealed: + actions = item.triggers[READ] + game.do_actions(actions) + else: + game.say("Can't read that.") + + class OpenTrigger(Trigger): @staticmethod def pattern() -> Pattern: @@ -192,8 +224,8 @@ class OpenTrigger(Trigger): if not item_name: game.say("Open what?") return - item = game.room.search_item_name(item_name.lower()) - if item and OPEN in item.triggers: + item = game.room.search_item_name(item_name) + if item and OPEN in item.triggers and item.revealed: actions = item.triggers[OPEN] game.do_actions(actions) else: @@ -216,8 +248,8 @@ class CloseTrigger(Trigger): if not item_name: game.say("Close what?") return - item = game.room.search_item_name(item_name.lower()) - if item and CLOSE in item.triggers: + item = game.room.search_item_name(item_name) + if item and CLOSE in item.triggers and item.revealed: actions = item.triggers[CLOSE] game.do_actions(actions) else: @@ -229,7 +261,7 @@ class GoTrigger(Trigger): def pattern() -> Pattern: return re.compile( r""" - (?Pgo|go[ ]*to|leave|exit) + (?Pgo([ ]*to)?|leave|exit) (((an?|the)[ ]+)?[ ]+(?P.+?))? """, re.IGNORECASE | re.VERBOSE, @@ -241,7 +273,9 @@ class GoTrigger(Trigger): otrigger = match["trigger"].lower().capitalize() game.say(f"{otrigger} where?") return - item = game.room.search_item_name(item_name.lower()) - if item and GO in item.triggers: + item = game.room.search_item_name(item_name) + if item and GO in item.triggers and item.revealed: actions = item.triggers[GO] game.do_actions(actions) + else: + game.say("Can't go there.") diff --git a/agame/util.py b/agame/util.py index 02c031d..a7283f3 100644 --- a/agame/util.py +++ b/agame/util.py @@ -6,6 +6,6 @@ if TYPE_CHECKING: def search_item_name(seq: Iterable["ItemInst"], item_name: str) -> Optional["ItemInst"]: for item in seq: - if item.name == item_name or item_name in item.synonyms: + if item.name.lower() == item_name.lower() or item_name.lower() in item.synonyms: return item return None diff --git a/examplegame/__init__.py b/examplegame/__init__.py index 91cc4e7..24bc6ed 100644 --- a/examplegame/__init__.py +++ b/examplegame/__init__.py @@ -7,13 +7,12 @@ from agame.trigger import * # This is the *game* here database = Database() -from . import items from . import rooms from . import vars # Build the game state game = Game( database=database, - room=database.rooms["start"], + room=database.rooms["inside_cabin"], vars=vars.vars, ) diff --git a/examplegame/items.py b/examplegame/items.py deleted file mode 100644 index b0f2f6b..0000000 --- a/examplegame/items.py +++ /dev/null @@ -1,72 +0,0 @@ -from agame.item import Item -from agame.trigger import * -from agame.action import * -from . import database - -database.add_items( - Item( - id="glowing_rock", - name="glowing rock", - desc="This rock is glowing.", - synonyms=("rock",), - room_desc="You see a ((glowing rock)). You have **got** to have it.", - triggers={ - GET: [ - PrintAction( - "You try to pick up the rock, but it slips out of your greasy hands.", - "Maybe you should wash your hands, you disgusting little man.", - ) - ], - LOOK: [PrintAction("Man, that rock looks awesome.")], - }, - ), - Item( - id="cell_door", - name="door", - room_desc="A ((door)) sits on the far wall.", - triggers={ - GET: [PrintAction("The door is pretty attached to its wall.")], - OPEN: [ - CheckVarAction( - "cell_door_open", - Compare.EQUALS, - True, - yes=[ - PrintAction( - "It's already open. You push on the door even //more//, just in case." - ), - SleepAction(1.0), - PrintAction("..."), - SleepAction(1.0), - PrintAction("Yup, still open."), - ], - no=[ - SetVarAction("cell_door_open", True), - PrintAction("The door swings open, thanks to you."), - ], - ) - ], - CLOSE: [ - CheckVarAction( - "cell_door_open", - Compare.EQUALS, - True, - yes=[ - PrintAction("You close that door. Nice job."), - SetVarAction("cell_door_open", False), - ], - no=[PrintAction("The door is already closed.")], - ) - ], - LOOK: [ - CheckVarAction( - "cell_door_open", - Compare.EQUALS, - True, - yes=[PrintAction("It's a door, wide open, because you opened it.")], - no=[PrintAction("A closed door. You can change this.")], - ) - ], - }, - ), -) diff --git a/examplegame/rooms.py b/examplegame/rooms.py index e7ac796..a0e76cf 100644 --- a/examplegame/rooms.py +++ b/examplegame/rooms.py @@ -1,14 +1,262 @@ +from agame.action import * +from agame.item import Item from agame.room import Room +from agame.trigger import * from . import database +################################################################################ +# Inside the cabin +################################################################################ + +database.add_items( + Item( + id="cabin_inside_door", + name="Cabin door", + room_desc="A door rests upon the south wall.", + synonyms=("door",), + triggers={ + LOOK: [PrintAction("A strong, sturdy door.")], + GET: [PrintAction("The door is firmly attached to the wall.")], + # Don't actually worry about the door being open here. It's just a + # nice flavoring. + GO: [PrintAction("You exit the cabin."), TeleportAction("outside_cabin")], + OPEN: [ + CheckVarAction( + "cabin_door_open", + yes=PrintAction("The door is already open."), + no=[ + PrintAction("You pull the cabin door open."), + SetVarAction("cabin_door_open", True), + ], + ) + ], + CLOSE: [ + CheckVarAction( + "cabin_door_open", + yes=[ + PrintAction("You shut the door tight."), + SetVarAction("cabin_door_open", False), + ], + no=PrintAction("The door is already closed."), + ) + ], + }, + ), + Item( + id="cabin_inside_bed", + name="Bed", + room_desc="A bed, freshly made, sits in the corner.", + triggers={ + LOOK: [ + PrintAction("It's a comfortable looking bed, but no time for rest now.") + ], + GET: [PrintAction("Uh, no.")], + USE: [ + PrintAction("You've only just woken up, now is not the time for rest.") + ], + GO: [ + PrintAction("You've only just woken up, now is not the time for rest.") + ], + }, + ), + Item( + # The outside view, and also kind of a hack for the player to be allowed + # to say "go outside" and "look outside" without having an "outside" + # synonym for the door, so the player can't also do "open outside" + id="cabin_inside_outside", + name="Outside", + room_desc=None, + triggers={ + LOOK: [PrintAction("It's brown and gray.")], + GO: [PrintAction("You exit the cabin."), TeleportAction("outside_cabin")], + }, + ), + Item( + id="cabin_inside_window", + name="Window", + room_desc="A dull light pours in from the solitary window on the east wall.", + synonyms=("outside window", "out window", "out the window"), + triggers={ + LOOK: [PrintAction("It's brown and gray.")], + OPEN: [ + PrintAction( + "This window is simply a pane of glass in a fixed frame, it be neither opened nor closed." + ) + ], + CLOSE: [ + PrintAction( + "This window is simply a pane of glass in a fixed frame, it be neither opened nor closed." + ) + ], + GO: [PrintAction("Try using the door instead.")], + }, + ), +) + +database.add_room( + Room( + id="inside_cabin", + name="Inside the cabin", + desc="", + items=[ + database.items["cabin_inside_door"].create_inst(), + database.items["cabin_inside_bed"].create_inst(), + database.items["cabin_inside_outside"].create_inst(), + database.items["cabin_inside_window"].create_inst(), + ], + ) +) + +################################################################################ +# Outside the cabin +################################################################################ + +ANJAS_LETTER = [ + "Dear Juni!", + "If you are reading this message you have finally arrived in the West, near where " + "the horrible machine resides. Turn it off, and its life-sapping force in the " + "world will cease.", + "You are our only hope - we are counting on you!", + "Anja", +] +database.add_items( + Item( + id="anjas_letter", + name="Anja's letter", + synonyms=("letter", "anjas letter", "anja's letter"), + triggers={ + GET: [GetAction("anjas_letter")], + LOOK: [PrintAction(*ANJAS_LETTER)], + READ: [PrintAction(*ANJAS_LETTER)], + }, + ), + Item( + id="mailbox", + name="Mailbox", + synonyms=("mailbox", "postbox", "rusty mailbox", "rusty postbox"), + room_desc="A rusty mailbox sits by the door to the cabin.", + triggers={ + GET: [ + PrintAction( + "The mailbox is firmly rooted in the ground. You don't have anywhere to put it, anyway." + ) + ], + LOOK: [ + CheckVarAction( + var_id="mailbox_open", + yes=[ + CheckRoomItemsAction( + item_ids="anjas_letter", + yes=PrintAction("A single letter sits in the mailbox."), + no=PrintAction("It's empty."), + ) + ], + no=[ + PrintAction("A rusty mailbox sits atop its post. It is closed.") + ], + ) + ], + OPEN: [ + CheckVarAction( + var_id="mailbox_open", + yes=PrintAction("The mailbox is already open."), + no=[ + PrintAction( + "You unlatch the lid to the mailbox, and it pops open." + ), + SetVarAction("mailbox_open", True), + CheckRoomItemsAction( + item_ids="anjas_letter", + yes=[ + PrintAction("A single letter sits inside."), + RevealItemAction("anjas_letter"), + ], + ), + ], + ) + ], + CLOSE: [ + CheckVarAction( + var_id="mailbox_open", + yes=[ + PrintAction("The mailbox snaps shut."), + SetVarAction("mailbox_open", False), + CheckRoomItemsAction( + "anjas_letter", yes=[UnrevealItemAction("anjas_letter")] + ), + ], + no=PrintAction("It's already closed."), + ) + ], + }, + ), + Item( + id="west_path", + name="Western path", + synonyms=( + "path to west", + "path to the west", + "path leading west", + "path leading westward", + "west", + "westward", + "westward path", + "west path", + "western path", + ), + room_desc=( + "A path leads westward, where the sky loses its color and becomes a dark gray. " + "The land becomes more barren in this direction." + ), + triggers={ + LOOK: [ + PrintAction( + "A dirt path extends towards a barren wasteland. At one " + "point it had become overgrown with disuse, but most anything " + "that was once alive is either long gone or a husk of its " + "former self." + ) + ], + GO: [PrintAction("You head west."), TeleportAction("western_fork")], + USE: [PrintAction("You head west."), TeleportAction("western_fork")], + }, + ), +) + database.add_rooms( Room( - id="start", - name="Test room", - desc="You're in ((Todd's Test Cell)).", + id="outside_cabin", + name="Outside the cabin", + desc=[], items=[ - database.items["glowing_rock"].create_inst(), - database.items["cell_door"].create_inst(), + database.items["mailbox"].create_inst(), + database.items["anjas_letter"].create_inst(revealed=False), + database.items["west_path"].create_inst(), ], ), ) + +################################################################################ +# Western fork +################################################################################ + +database.add_items( + Item( + id="eastern_path", + name="Eastern path", + room_desc="A path that leads eastward. Your cabin lies in this direction.", + synonyms=( + "path to east", + "path to the east", + "path leading east", + "path leading eastward", + "east", + "eastward", + "eastward path", + "east path", + "eastern path", + ), + ) +) + +database.add_rooms(Room(id="western_fork", name="Western fork", desc=[], items=[])) diff --git a/examplegame/vars.py b/examplegame/vars.py index 78b9646..906fa3a 100644 --- a/examplegame/vars.py +++ b/examplegame/vars.py @@ -3,5 +3,6 @@ Define all game variables here. """ vars = { - "cell_door_open": False, + "cabin_door_open": False, + "mailbox_open": False, }