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

View File

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

View File

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

View File

@@ -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=[]))

View File

@@ -3,5 +3,6 @@ Define all game variables here.
"""
vars = {
"cell_door_open": False,
"cabin_door_open": False,
"mailbox_open": False,
}