Rudimentary user input for dialogs has been added. * Displays must add support for showing dialog options, and receiving user input for dialog selections. * A selected dialog can optionally perform actions, and then directs the game into the next dialog state. * Dialog options may be conditionally shown with variables. Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
475 lines
13 KiB
Python
475 lines
13 KiB
Python
import dataclasses
|
|
import time
|
|
from typing import Any, Mapping, Optional, Sequence, Union, TYPE_CHECKING
|
|
import enum
|
|
|
|
if TYPE_CHECKING:
|
|
from agame.game import Game
|
|
from agame.dialog import DialogOption
|
|
|
|
|
|
__all__ = (
|
|
"Action",
|
|
"SleepAction",
|
|
"PrintAction",
|
|
"PrintRoomAction",
|
|
"PlayerInputAction",
|
|
"WaitForAckAction",
|
|
"TeleportAction",
|
|
"GetAction",
|
|
"CheckInvItemsAction",
|
|
"CheckRoomItemsAction",
|
|
"Compare",
|
|
"Var",
|
|
"CheckVarAction",
|
|
"SetVarAction",
|
|
"RevealItemAction",
|
|
"UnrevealItemAction",
|
|
"DialogAction",
|
|
)
|
|
|
|
|
|
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.NOT_EQUALS,
|
|
against: Any = False,
|
|
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
|
|
|
|
|
|
@dataclasses.dataclass
|
|
class DialogAction(Action):
|
|
"""
|
|
Displays a new dialog.
|
|
"""
|
|
|
|
dialog: Mapping[str, Sequence["DialogOption"]]
|
|
start: str
|
|
|
|
def act(self, game: "Game"):
|
|
selection_id: Optional[str] = self.start
|
|
|
|
while selection_id:
|
|
# Figure out which options should appear
|
|
options = [
|
|
opt
|
|
for opt in self.dialog[selection_id]
|
|
# this is a logical implication
|
|
# required_var -> (game.vars[opt.required_var])
|
|
if not opt.required_var
|
|
or (opt.required_var and game.vars[opt.required_var])
|
|
]
|
|
# Get the selection
|
|
selection = game.display.dialog_options(options)
|
|
selection_id = selection.next
|
|
# Do dialog actions
|
|
game.do_actions(selection.actions)
|