Initial commit with a mostly working engine.
Basic commands are being parsed. I think the only weird part is the 'use' command because it needs to possibly target two things. A tiny test example is provided in __main__, this will probably be broken out later. Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
This commit is contained in:
0
agame/__init__.py
Normal file
0
agame/__init__.py
Normal file
115
agame/__main__.py
Normal file
115
agame/__main__.py
Normal file
@@ -0,0 +1,115 @@
|
||||
from agame.action import *
|
||||
from agame.game import *
|
||||
from agame.item import *
|
||||
from agame.room import *
|
||||
from agame.trigger import *
|
||||
|
||||
|
||||
# TODO - take a custom module name that has:
|
||||
# .database
|
||||
# .game
|
||||
# so that you can just RUN it
|
||||
# Something similar to wsgi using "app" in the passed module
|
||||
|
||||
|
||||
# This is the *game* here
|
||||
database = 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.")],
|
||||
)
|
||||
],
|
||||
},
|
||||
),
|
||||
)
|
||||
database.add_rooms(
|
||||
Room(
|
||||
id="start",
|
||||
name="Test room",
|
||||
desc="You're in ((Todd's Test Cell)).",
|
||||
items=[
|
||||
database.items["glowing_rock"].create_inst(),
|
||||
database.items["cell_door"].create_inst(),
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
# Build the game state
|
||||
game = Game(
|
||||
database=database,
|
||||
room=database.rooms["start"],
|
||||
vars={
|
||||
"cell_door_open": False,
|
||||
},
|
||||
)
|
||||
|
||||
game.print_room()
|
||||
while True:
|
||||
try:
|
||||
game.say()
|
||||
line = input("> ")
|
||||
game.say()
|
||||
game.run_command(line)
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
game.say()
|
||||
game.say("Bye.")
|
||||
break
|
||||
294
agame/action.py
Normal file
294
agame/action.py
Normal file
@@ -0,0 +1,294 @@
|
||||
import dataclasses
|
||||
import time
|
||||
from typing import Any, Optional, Sequence, Union, TYPE_CHECKING
|
||||
import enum
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from agame.game import Game
|
||||
|
||||
|
||||
__all__ = (
|
||||
"Action",
|
||||
"SleepAction",
|
||||
"PrintAction",
|
||||
"TeleportAction",
|
||||
"GetAction",
|
||||
"CheckInvItemsAction",
|
||||
"CheckRoomItemsAction",
|
||||
"Compare",
|
||||
"Var",
|
||||
"CheckVarAction",
|
||||
"SetVarAction",
|
||||
)
|
||||
|
||||
|
||||
class Action:
|
||||
"""
|
||||
An action that the game engine can take.
|
||||
|
||||
This is the base class. It can be instantiated as a "dummy" action if
|
||||
needed.
|
||||
"""
|
||||
|
||||
def act(self, game: "Game"):
|
||||
"""
|
||||
Complete this action.
|
||||
"""
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class SleepAction(Action):
|
||||
"""
|
||||
An action that delays the amount of time, in seconds. Decimal values are
|
||||
allowed.
|
||||
"""
|
||||
|
||||
secs: float
|
||||
|
||||
def act(self, game: "Game"):
|
||||
time.sleep(self.secs)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class PrintAction(Action):
|
||||
"""
|
||||
Prints a message to the screen.
|
||||
"""
|
||||
|
||||
lines: Sequence[str]
|
||||
|
||||
def __init__(self, *lines: str):
|
||||
self.lines = lines
|
||||
|
||||
def act(self, game: "Game"):
|
||||
if not self.lines:
|
||||
return
|
||||
line = self.lines[0]
|
||||
game.say(line)
|
||||
for line in self.lines[1:]:
|
||||
game.say()
|
||||
game.say(line)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class TeleportAction(Action):
|
||||
"""
|
||||
Moves the player to another room.
|
||||
"""
|
||||
|
||||
room_id: str
|
||||
|
||||
def act(self, game: "Game"):
|
||||
# TODO
|
||||
raise NotImplementedError("TODO - implement teleport action")
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class GetAction(Action):
|
||||
"""
|
||||
Removes an item from the current room and puts it in the player's inventory.
|
||||
"""
|
||||
|
||||
item_id: str
|
||||
pickup_text: Optional[str] = None
|
||||
|
||||
# def __init__(self, item_id: str, pickup_text: )
|
||||
|
||||
def act(self, game: "Game"):
|
||||
# Find the first instance of the item in the room and remove it
|
||||
item = game.room.remove(self.item_id)
|
||||
assert (
|
||||
item is not None
|
||||
), f"attempted to remove an item (id: {self.item_id}) that does not exist in the current room; this is a game logic error/bug"
|
||||
|
||||
if item.id in game.inventory:
|
||||
# Item in inventory already? Update count
|
||||
game.inventory[item.id].count += item.count
|
||||
else:
|
||||
# Otherwise just add it
|
||||
game.inventory[item.id] = item
|
||||
|
||||
# Print text
|
||||
if self.pickup_text is None:
|
||||
game.say(f"You pick up (({item.name})).")
|
||||
else:
|
||||
game.say(self.pickup_text)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class CheckInvItemsAction(Action):
|
||||
"""
|
||||
Checks if all supplied items are present in the inventory, and executes the
|
||||
appropriate action sequence.
|
||||
"""
|
||||
|
||||
item_ids: Union[str, Sequence[str]]
|
||||
yes: Sequence[Action]
|
||||
no: Sequence[Action]
|
||||
|
||||
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
|
||||
for item_id in items:
|
||||
if item_id not in game.inventory:
|
||||
game.do_actions(self.no)
|
||||
return
|
||||
game.do_actions(self.yes)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class CheckRoomItemsAction(Action):
|
||||
"""
|
||||
Checks if all supplied items are present in the current room, and executes the
|
||||
appropriate action sequence.
|
||||
"""
|
||||
|
||||
item_ids: str
|
||||
yes: Sequence[Action]
|
||||
no: Sequence[Action]
|
||||
|
||||
def act(self, game: "Game"):
|
||||
items = [self.item_ids] if isinstance(self.item_ids, str) else self.item_ids
|
||||
for item_id in items:
|
||||
if item_id not in game.room.items:
|
||||
game.do_actions(self.no)
|
||||
return
|
||||
game.do_actions(self.yes)
|
||||
|
||||
|
||||
class Compare(enum.Enum):
|
||||
"""
|
||||
A comparison for a value.
|
||||
"""
|
||||
|
||||
# Does what it says on the tin.
|
||||
#
|
||||
# This is type-sensitive and literally just does `==` in Python. Seriously.
|
||||
EQUALS = enum.auto()
|
||||
|
||||
# Also does what it says on the tin.
|
||||
#
|
||||
# This is type-sensitive and literally just does `!=` in Python. Seriously.
|
||||
NOT_EQUALS = enum.auto()
|
||||
|
||||
# Checks if a value is less than to another value.
|
||||
#
|
||||
# This uses the `cmp` to check the values.
|
||||
LESS_THAN = enum.auto()
|
||||
|
||||
# Checks if a value is less than or equal to another value.
|
||||
#
|
||||
# This uses the `cmp` to check the values.
|
||||
LESS_THAN_EQUALS = enum.auto()
|
||||
|
||||
# Checks if a value is greater than to another value.
|
||||
#
|
||||
# This uses the `cmp` to check the values.
|
||||
GREATER_THAN = enum.auto()
|
||||
|
||||
# Checks if a value is greater than or equal to another value.
|
||||
#
|
||||
# This uses the `cmp` to check the values.
|
||||
GREATER_THAN_EQUALS = enum.auto()
|
||||
|
||||
# Checks if the string-ified version of the value matches the other value,
|
||||
# as a regex.
|
||||
MATCHES = enum.auto()
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Var:
|
||||
id: str
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class CheckVarAction(Action):
|
||||
"""
|
||||
Check a variable's value using some kind of comparison.
|
||||
|
||||
If you want to check one variable against another, use the `action.Var`
|
||||
type.
|
||||
"""
|
||||
|
||||
# The variable to check the value of.
|
||||
var_id: str
|
||||
|
||||
# The comparison operation to use. See `Compare` for comparisons.
|
||||
compare: Compare
|
||||
|
||||
# The constant value to check against.
|
||||
against: Any
|
||||
|
||||
# The action to execute when this is true.
|
||||
yes: Sequence[Action]
|
||||
|
||||
# The action to execute when this is false.
|
||||
no: Sequence[Action]
|
||||
|
||||
def act(self, game: "Game"):
|
||||
val = game.vars.get(self.var_id, None)
|
||||
compare_val = None
|
||||
if isinstance(self.against, Var):
|
||||
compare_val = game.vars.get(self.against.id, None)
|
||||
else:
|
||||
compare_val = self.against
|
||||
|
||||
result = False
|
||||
if self.compare == Compare.EQUALS:
|
||||
result = val == compare_val
|
||||
elif self.compare == Compare.NOT_EQUALS:
|
||||
result = val != compare_val
|
||||
elif self.compare == Compare.LESS_THAN:
|
||||
if isinstance(val, str) or isinstance(compare_val, str):
|
||||
result = str(val) < str(compare_val)
|
||||
elif (isinstance(val, int) or isinstance(val, float)) and (
|
||||
isinstance(compare_val, int) or isinstance(compare_val, float)
|
||||
):
|
||||
result = val < compare_val
|
||||
elif self.compare == Compare.LESS_THAN_EQUALS:
|
||||
if isinstance(val, str) or isinstance(compare_val, str):
|
||||
result = str(val) <= str(compare_val)
|
||||
elif (isinstance(val, int) or isinstance(val, float)) and (
|
||||
isinstance(compare_val, int) or isinstance(compare_val, float)
|
||||
):
|
||||
result = val <= compare_val
|
||||
elif self.compare == Compare.GREATER_THAN:
|
||||
if isinstance(val, str) or isinstance(compare_val, str):
|
||||
result = str(val) > str(compare_val)
|
||||
elif (isinstance(val, int) or isinstance(val, float)) and (
|
||||
isinstance(compare_val, int) or isinstance(compare_val, float)
|
||||
):
|
||||
result = val > compare_val
|
||||
elif self.compare == Compare.GREATER_THAN_EQUALS:
|
||||
if isinstance(val, str) or isinstance(compare_val, str):
|
||||
result = str(val) >= str(compare_val)
|
||||
elif (isinstance(val, int) or isinstance(val, float)) and (
|
||||
isinstance(compare_val, int) or isinstance(compare_val, float)
|
||||
):
|
||||
result = val >= compare_val
|
||||
elif self.compare == Compare.MATCHES:
|
||||
pass
|
||||
else:
|
||||
assert False, f"{self.compare} isn't a Compare value, ya doink"
|
||||
|
||||
# Check result, do action
|
||||
if result:
|
||||
game.do_actions(self.yes)
|
||||
else:
|
||||
game.do_actions(self.no)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class SetVarAction(Action):
|
||||
"""
|
||||
Set a variable to a specific value.
|
||||
"""
|
||||
|
||||
var_id: str
|
||||
value: Any
|
||||
|
||||
def act(self, game: "Game"):
|
||||
value = (
|
||||
game.vars.get(self.value.id) if isinstance(self.value, Var) else self.value
|
||||
)
|
||||
game.vars[self.var_id] = value
|
||||
39
agame/color.py
Normal file
39
agame/color.py
Normal file
@@ -0,0 +1,39 @@
|
||||
import re
|
||||
|
||||
|
||||
__all__ = ("colorize",)
|
||||
|
||||
BOLD_PAT = re.compile(r"\*\*(.+?)\*\*", re.MULTILINE)
|
||||
ITALIC_PAT = re.compile(r"//(.+?)//", re.MULTILINE)
|
||||
INTEREST_PAT = re.compile(r"\(\((.+?)\)\)", re.MULTILINE)
|
||||
SHADOW_PAT = re.compile(r"\{\{(.+?)\}\}", re.MULTILINE)
|
||||
|
||||
BOLD_COL = "\u001b[1m"
|
||||
ITALIC_COL = "\u001b[3m"
|
||||
INTEREST_COL = "\u001b[34;1m"
|
||||
SHADOW_COL = "\u001b[30;1m"
|
||||
RESET_COL = "\u001b[0m"
|
||||
|
||||
|
||||
def colorize(text: str) -> str:
|
||||
"""
|
||||
Colorizes text for output on an ANSI terminal.
|
||||
|
||||
This will use escape codes to replace things.
|
||||
|
||||
Style guide:
|
||||
((This)) is "interest" styling. This will make the text blue.
|
||||
{{This}} is "shadow" styling. This will make the text a dark grey (or at
|
||||
least, more subtle.)
|
||||
"""
|
||||
replacements = [
|
||||
(INTEREST_PAT, INTEREST_COL),
|
||||
(SHADOW_PAT, SHADOW_COL),
|
||||
(BOLD_PAT, BOLD_COL),
|
||||
(ITALIC_PAT, ITALIC_COL),
|
||||
]
|
||||
|
||||
for (pat, col) in replacements:
|
||||
text = pat.sub(col + r"\1" + RESET_COL, text)
|
||||
|
||||
return text
|
||||
113
agame/game.py
Normal file
113
agame/game.py
Normal file
@@ -0,0 +1,113 @@
|
||||
import dataclasses
|
||||
import textwrap
|
||||
from typing import Any, MutableMapping, Match, Optional, Sequence
|
||||
from agame.action import Action
|
||||
from agame.color import colorize
|
||||
from agame.item import Item, ItemInst
|
||||
from agame.room import Room
|
||||
from agame.trigger import *
|
||||
|
||||
|
||||
__all__ = (
|
||||
"Database",
|
||||
"Game",
|
||||
)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Database:
|
||||
# All items available to the game.
|
||||
items: MutableMapping[str, Item] = dataclasses.field(default_factory=dict)
|
||||
# All rooms available to the game.
|
||||
rooms: MutableMapping[str, Room] = dataclasses.field(default_factory=dict)
|
||||
|
||||
def add_item(self, item: Item):
|
||||
self.items[item.id] = item
|
||||
|
||||
def add_items(self, *items: Item):
|
||||
for item in items:
|
||||
self.add_item(item)
|
||||
|
||||
def add_room(self, room: Room):
|
||||
self.rooms[room.id] = room
|
||||
|
||||
def add_rooms(self, *rooms: Room):
|
||||
for room in rooms:
|
||||
self.add_room(room)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Game:
|
||||
# Game room/items database
|
||||
database: Database
|
||||
# Current room.
|
||||
room: Room
|
||||
# Player inventory.
|
||||
inventory: MutableMapping[str, ItemInst] = dataclasses.field(default_factory=dict)
|
||||
# Variables.
|
||||
vars: MutableMapping[str, Any] = dataclasses.field(default_factory=dict)
|
||||
|
||||
def run_command(self, line: str):
|
||||
line = line.strip()
|
||||
if not line:
|
||||
return
|
||||
|
||||
triggers: Sequence[Trigger] = [
|
||||
GetTrigger(),
|
||||
UseTrigger(),
|
||||
PutTrigger(),
|
||||
LookTrigger(),
|
||||
OpenTrigger(),
|
||||
CloseTrigger(),
|
||||
GoTrigger(),
|
||||
]
|
||||
|
||||
trigger = None
|
||||
match: Optional[Match] = None
|
||||
for t in triggers:
|
||||
match = t.pattern().fullmatch(line)
|
||||
if match:
|
||||
trigger = t
|
||||
break
|
||||
if not trigger:
|
||||
self.say("I'm not sure what you mean.")
|
||||
return
|
||||
|
||||
assert match, "why were no patterns matched?"
|
||||
trigger.trigger(self, match)
|
||||
|
||||
def do_actions(self, actions: Sequence[Action]):
|
||||
"Executes the supplied actions."
|
||||
for action in actions:
|
||||
action.act(self)
|
||||
|
||||
def print_room(self):
|
||||
"Prints this room's description."
|
||||
self.say(self.room.name)
|
||||
self.say()
|
||||
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:
|
||||
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):
|
||||
"Format, colorize, wrap, and print the message."
|
||||
message = message or ""
|
||||
message = textwrap.fill(message)
|
||||
print(colorize(message))
|
||||
|
||||
@property
|
||||
def rooms(self):
|
||||
"Shortcut property for `game.database.rooms`."
|
||||
return self.database.rooms
|
||||
|
||||
@property
|
||||
def items(self):
|
||||
"Shortcut property for `game.database.items`."
|
||||
return self.database.items
|
||||
131
agame/item.py
Normal file
131
agame/item.py
Normal file
@@ -0,0 +1,131 @@
|
||||
import dataclasses
|
||||
from typing import Mapping, Optional, Sequence
|
||||
from agame.action import Action
|
||||
|
||||
|
||||
__all__ = (
|
||||
"ItemInst",
|
||||
"Item",
|
||||
)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class ItemInst:
|
||||
"""
|
||||
An instance of an item in the game.
|
||||
"""
|
||||
|
||||
# Reference to the global item that this is an instance of.
|
||||
item: "Item"
|
||||
|
||||
# Gets whether this item can be taken.
|
||||
fixed: bool = False
|
||||
|
||||
# Gets how many of this item instance are present in this stack.
|
||||
count: int = 1
|
||||
|
||||
# Gets whether this item is revealed or not.
|
||||
revealed: bool = True
|
||||
|
||||
@property
|
||||
def id(self) -> str:
|
||||
return self.item.id
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self.item.name
|
||||
|
||||
@property
|
||||
def desc(self) -> Optional[str]:
|
||||
return self.item.desc
|
||||
|
||||
@property
|
||||
def synonyms(self) -> Sequence[str]:
|
||||
return self.item.synonyms
|
||||
|
||||
@property
|
||||
def room_desc(self) -> Optional[str]:
|
||||
return self.item.room_desc
|
||||
|
||||
@property
|
||||
def triggers(self) -> Mapping[str, Sequence[Action]]:
|
||||
return self.item.triggers
|
||||
|
||||
@property
|
||||
def use_actions(self) -> Mapping[str, Sequence[Action]]:
|
||||
return self.item.use_actions
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Item:
|
||||
"""
|
||||
A game item.
|
||||
"""
|
||||
|
||||
# The ID of this item. This is how items are looked up in the game.
|
||||
id: str
|
||||
|
||||
# The printable name of this item.
|
||||
name: str
|
||||
|
||||
# A long description for this item.
|
||||
desc: Optional[str] = None
|
||||
|
||||
# A list of all synonyms for this item.
|
||||
synonyms: Sequence[str] = dataclasses.field(default_factory=list)
|
||||
|
||||
# The description that is used in the context of a room's `look` command.
|
||||
#
|
||||
# When someone wants to look at the entire room, all items that have been
|
||||
# revealed will also be displayed.
|
||||
#
|
||||
# If you want to disable this behavior entirely, `room_desc` should be set
|
||||
# to `None`.
|
||||
#
|
||||
# If you want to use the default text, "You see a (({item.name}))",
|
||||
# `room_desc` should be set to the blank string, `""`.
|
||||
#
|
||||
# Otherwise, the `room_desc` string will be colorized and printed as
|
||||
# written.
|
||||
room_desc: Optional[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
|
||||
# trigger.
|
||||
#
|
||||
# Valid triggers include:
|
||||
# * get
|
||||
# * use
|
||||
# * put
|
||||
# * look
|
||||
# * open
|
||||
# * close
|
||||
# ...more to come
|
||||
triggers: Mapping[str, Sequence[Action]] = dataclasses.field(default_factory=dict)
|
||||
|
||||
# A mapping of other items that this item may be used with, specifically on
|
||||
# the USE command.
|
||||
#
|
||||
# USE is a strange beast, because instead of just one implicit target (which
|
||||
# is verified to exist by the trigger, of all things) we have *two* targets:
|
||||
# the subject and the direct object. And not always!
|
||||
#
|
||||
# For example, we have an item, paintbrush. These are some options:
|
||||
# use paintbrush # <- on what?
|
||||
# use paintbrush on canvas # <- you paint a beautiful masterpiece.
|
||||
# use paintbrush on car # <- that's not allowed (custom text)
|
||||
# use paintbrush on fake item # <- that item doesn't exist
|
||||
#
|
||||
# We can't really represent this with the current str -> sequence[action]
|
||||
# stuff we have in place right now, so a special field will be good enough
|
||||
# until a more insane/robust solution is implemented.
|
||||
use_actions: Mapping[str, Sequence[Action]] = dataclasses.field(
|
||||
default_factory=dict
|
||||
)
|
||||
|
||||
def create_inst(self, *args, **kwargs):
|
||||
"""
|
||||
Creates a new item instance, passing the supplied args and kwargs to the
|
||||
ItemInst constructor.
|
||||
"""
|
||||
return ItemInst(item=self, *args, **kwargs)
|
||||
46
agame/room.py
Normal file
46
agame/room.py
Normal file
@@ -0,0 +1,46 @@
|
||||
import dataclasses
|
||||
from typing import MutableMapping, Optional, Sequence, Union, TYPE_CHECKING
|
||||
from agame.item import ItemInst
|
||||
from agame.util import search_item_name
|
||||
|
||||
|
||||
__all__ = ("Room",)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Room:
|
||||
id: str
|
||||
name: str
|
||||
desc: str
|
||||
items: MutableMapping[str, ItemInst]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
id: str,
|
||||
name: str,
|
||||
desc: str,
|
||||
items: Union[Sequence[ItemInst], MutableMapping[str, ItemInst]],
|
||||
):
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.desc = desc
|
||||
if isinstance(items, MutableMapping):
|
||||
self.items = items
|
||||
else:
|
||||
self.items = {item.id: item for item in items}
|
||||
|
||||
def search_item_name(self, item_name: str) -> Optional[ItemInst]:
|
||||
"""
|
||||
Searches all item instances in the room for the given item name, also
|
||||
checking synonyms. Returns the first item instance found, or none if no
|
||||
synonyms or names were found to match.
|
||||
"""
|
||||
return search_item_name(self.items.values(), item_name)
|
||||
|
||||
def remove(self, item_id: str) -> Optional[ItemInst]:
|
||||
"""
|
||||
Removes an item with the given ID from the room, returning it.
|
||||
|
||||
If it's not present in the room, `None` is returned.
|
||||
"""
|
||||
return self.items.pop(item_id, None)
|
||||
247
agame/trigger.py
Normal file
247
agame/trigger.py
Normal file
@@ -0,0 +1,247 @@
|
||||
import abc
|
||||
import re
|
||||
from typing import Match, Pattern, TYPE_CHECKING
|
||||
from agame.util import search_item_name
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from agame.game import Game
|
||||
|
||||
|
||||
# Triggers:
|
||||
# get/take/grab/pick up [a[n]/the] x
|
||||
# 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]
|
||||
# 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
|
||||
# (put down)/drop [a[n]/the] x
|
||||
__all__ = (
|
||||
"Trigger",
|
||||
"GetTrigger",
|
||||
"UseTrigger",
|
||||
"PutTrigger",
|
||||
"LookTrigger",
|
||||
"OpenTrigger",
|
||||
"CloseTrigger",
|
||||
"GoTrigger",
|
||||
"GET",
|
||||
"USE",
|
||||
"PUT",
|
||||
"LOOK",
|
||||
"OPEN",
|
||||
"CLOSE",
|
||||
"GO",
|
||||
)
|
||||
GET = "get"
|
||||
USE = "use"
|
||||
PUT = "put"
|
||||
LOOK = "look"
|
||||
OPEN = "open"
|
||||
CLOSE = "close"
|
||||
GO = "go"
|
||||
|
||||
|
||||
class Trigger(metaclass=abc.ABCMeta):
|
||||
@staticmethod
|
||||
@abc.abstractmethod
|
||||
def pattern() -> Pattern:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def trigger(self, game: "Game", match: Match):
|
||||
pass
|
||||
|
||||
|
||||
class GetTrigger(Trigger):
|
||||
@staticmethod
|
||||
def pattern() -> Pattern:
|
||||
return re.compile(
|
||||
r"""
|
||||
(?P<trigger>get|take|grab|pick[ ]*up)
|
||||
(([ ]+(an?|the))?[ ]+(?P<item>.+))?
|
||||
""",
|
||||
re.IGNORECASE | re.VERBOSE,
|
||||
)
|
||||
|
||||
def trigger(self, game: "Game", match: Match):
|
||||
item_name = match["item"]
|
||||
if not item_name:
|
||||
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:
|
||||
actions = item.triggers[GET]
|
||||
# if there are any actions, do them. else do the default action
|
||||
game.do_actions(actions)
|
||||
else:
|
||||
game.say("Can't get that.")
|
||||
|
||||
|
||||
class UseTrigger(Trigger):
|
||||
@staticmethod
|
||||
def pattern() -> Pattern:
|
||||
# TODO(low) - wouldn't it be cool to specify "use" actions?
|
||||
# e.g. you have a gun item and you want to be allowed to use "shoot" in order to
|
||||
# use the gun.
|
||||
return re.compile(
|
||||
r"""
|
||||
(?P<trigger>use)
|
||||
(([ ]+(an?|the))?[ ]+(?P<item>.+?)
|
||||
([ ]+(with|on)([ ]+(an?|the))?[ ]+(?P<target>.+))?)?
|
||||
""",
|
||||
re.IGNORECASE | re.VERBOSE,
|
||||
)
|
||||
|
||||
def trigger(self, game: "Game", match: Match):
|
||||
item_name = match["item"]
|
||||
if not item_name:
|
||||
game.say("Use what?")
|
||||
return
|
||||
target_name = match["target"]
|
||||
|
||||
# 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()
|
||||
)
|
||||
if not item:
|
||||
game.say(f"I'm not sure what you mean by {item_name}.")
|
||||
return
|
||||
|
||||
target = None
|
||||
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()
|
||||
)
|
||||
if not target:
|
||||
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:
|
||||
game.do_actions(item.use_actions[target.id])
|
||||
else:
|
||||
game.say("I'm not sure how to do that.")
|
||||
elif USE in item.triggers:
|
||||
# Check if the item can be used by itself
|
||||
game.do_actions(item.triggers[USE])
|
||||
elif item.use_actions:
|
||||
# This item can be used with *something*, but we don't know what.
|
||||
game.say(f"Use (({item_name})) with what?")
|
||||
else:
|
||||
# This can't be used.
|
||||
game.say("I can't really use that.")
|
||||
|
||||
|
||||
class PutTrigger(Trigger):
|
||||
@staticmethod
|
||||
def pattern() -> Pattern:
|
||||
return re.compile(
|
||||
r"""
|
||||
(?P<trigger>put)
|
||||
([ ]+((an?|the)[ ]+)?(?P<item>.+?)
|
||||
((on|in)[ ]+)?((an?|the)[ ]+)?[ ]+(?P<target>.+))?
|
||||
""",
|
||||
re.IGNORECASE | re.VERBOSE,
|
||||
)
|
||||
|
||||
def trigger(self, game: "Game", match: Match):
|
||||
item_name = match["item"]
|
||||
|
||||
|
||||
class LookTrigger(Trigger):
|
||||
@staticmethod
|
||||
def pattern() -> Pattern:
|
||||
return re.compile(
|
||||
r"""
|
||||
(?P<trigger>look)
|
||||
([ ]+(at[ ]+)?((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.lower())
|
||||
if item and LOOK in item.triggers:
|
||||
actions = item.triggers[LOOK]
|
||||
game.do_actions(actions)
|
||||
else:
|
||||
game.say("Can't see that.")
|
||||
|
||||
|
||||
class OpenTrigger(Trigger):
|
||||
@staticmethod
|
||||
def pattern() -> Pattern:
|
||||
return re.compile(
|
||||
r"""
|
||||
(?P<trigger>open)
|
||||
(((an?|the)[ ]+)?[ ]+(?P<item>.+?))?
|
||||
""",
|
||||
re.IGNORECASE | re.VERBOSE,
|
||||
)
|
||||
|
||||
def trigger(self, game: "Game", match: Match):
|
||||
item_name = match["item"]
|
||||
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:
|
||||
actions = item.triggers[OPEN]
|
||||
game.do_actions(actions)
|
||||
else:
|
||||
game.say("Can't open that.")
|
||||
|
||||
|
||||
class CloseTrigger(Trigger):
|
||||
@staticmethod
|
||||
def pattern() -> Pattern:
|
||||
return re.compile(
|
||||
r"""
|
||||
(?P<trigger>close)
|
||||
(((an?|the)[ ]+)?[ ]+(?P<item>.+?))?
|
||||
""",
|
||||
re.IGNORECASE | re.VERBOSE,
|
||||
)
|
||||
|
||||
def trigger(self, game: "Game", match: Match):
|
||||
item_name = match["item"]
|
||||
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:
|
||||
actions = item.triggers[CLOSE]
|
||||
game.do_actions(actions)
|
||||
else:
|
||||
game.say("Can't close that.")
|
||||
|
||||
|
||||
class GoTrigger(Trigger):
|
||||
@staticmethod
|
||||
def pattern() -> Pattern:
|
||||
return re.compile(
|
||||
r"""
|
||||
(?P<trigger>go|go[ ]*to|leave|exit)
|
||||
(((an?|the)[ ]+)?[ ]+(?P<item>.+?))?
|
||||
""",
|
||||
re.IGNORECASE | re.VERBOSE,
|
||||
)
|
||||
|
||||
def trigger(self, game: "Game", match: Match):
|
||||
item_name = match["item"]
|
||||
if not item_name:
|
||||
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:
|
||||
actions = item.triggers[GO]
|
||||
game.do_actions(actions)
|
||||
11
agame/util.py
Normal file
11
agame/util.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from typing import Iterable, Optional, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from agame.item import ItemInst
|
||||
|
||||
|
||||
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:
|
||||
return item
|
||||
return None
|
||||
Reference in New Issue
Block a user