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 <alekratz@gmail.com>
This commit is contained in:
104
agame/action.py
104
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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"""
|
||||
(?P<trigger>read)
|
||||
([ ]+((an?|the)[ ]+)?(?P<item>.+?))?
|
||||
""",
|
||||
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"""
|
||||
(?P<trigger>go|go[ ]*to|leave|exit)
|
||||
(?P<trigger>go([ ]*to)?|leave|exit)
|
||||
(((an?|the)[ ]+)?[ ]+(?P<item>.+?))?
|
||||
""",
|
||||
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.")
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user