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:
2021-11-18 16:26:16 -08:00
parent 1304b27944
commit 7f86aafc05
10 changed files with 446 additions and 121 deletions

View File

@@ -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

View File

@@ -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):

View File

@@ -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

View File

@@ -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

View File

@@ -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.")

View File

@@ -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