Files
ages/agame/action.py
Alek Ratzloff e868d0e14f Add display abstraction
In case we want to run this on something that isn't an ANSI terminal, we
have the option to implement it however we want.

Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
2021-11-20 19:38:06 -08:00

444 lines
12 KiB
Python

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",
"PrintRoomAction",
"PlayerInputAction",
"WaitForAckAction",
"TeleportAction",
"GetAction",
"CheckInvItemsAction",
"CheckRoomItemsAction",
"Compare",
"Var",
"CheckVarAction",
"SetVarAction",
"RevealItemAction",
"UnrevealItemAction",
)
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 PrintRoomAction(Action):
"""
Prints the current room.
"""
def act(self, game: "Game"):
game.say("", "." * 70, "")
game.print_room()
@dataclasses.dataclass
class PlayerInputAction(Action):
"""
Waits for a user to press a key, or press enter, or whatever.
"""
store_var_id: Optional[str] = None
def act(self, game: "Game"):
line = game.display.input()
if self.store_var_id:
assert (
self.store_var_id in game.vars
), f"could not find var with id {self.store_var_id}"
game.vars[self.store_var_id] = line
class WaitForAckAction(Action):
"""
Waits for a player to press a single key, or press enter, to acknowledge and
continue processing.
This is basically "press any key to continue..." territory.
"""
def act(self, game: "Game"):
game.display.wait_for_ack()
@dataclasses.dataclass
class TeleportAction(Action):
"""
Moves the player to another room.
"""
# The room ID to teleport to.
room_id: str
# Whether to "announce" the teleportation; i.e., print out the room
# description after teleporting to it.
announce: bool = True
def act(self, game: "Game"):
assert (
self.room_id in game.database.rooms
), f"could not find room with id {self.room_id}"
game.teleport(game.database.rooms[self.room_id])
if self.announce:
game.say("", "." * 70, "")
game.print_room()
@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 __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
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: 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:
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 __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
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"):
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