Add dialog actions

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>
This commit is contained in:
2021-11-28 17:27:05 -08:00
parent def9b584a8
commit 2357757b23
5 changed files with 173 additions and 13 deletions

View File

@@ -1,10 +1,11 @@
import dataclasses
import time
from typing import Any, Optional, Sequence, Union, TYPE_CHECKING
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__ = (
@@ -24,6 +25,7 @@ __all__ = (
"SetVarAction",
"RevealItemAction",
"UnrevealItemAction",
"DialogAction",
)
@@ -441,3 +443,32 @@ class UnrevealItemAction(Action):
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)

40
agame/dialog.py Normal file
View File

@@ -0,0 +1,40 @@
import dataclasses
from typing import Mapping, Optional, Sequence, TYPE_CHECKING
if TYPE_CHECKING:
from agame.action import Action
@dataclasses.dataclass
class DialogOption:
# The text for this dialog option.
text: str
# The actions that get executed upon choosing this dialog option.
actions: Sequence["Action"] = dataclasses.field(default_factory=list)
# The variable required for being able to display this dialog option.
#
# This variable must not be equal to `False`.
required_var: Optional[str] = None
# The next dialog that is chosen.
next: Optional[str] = None
{
"main": {
"type": "reply",
"options": [
{
"text": "What happened here?",
"next": "main",
"actions": [],
},
{
"text": "Bye.",
},
],
},
"bye": {
"type": "print",
"text": "Mhm.",
},
}

View File

@@ -1,4 +1,7 @@
import platform
from typing import Optional, Sequence, Tuple
from agame.action import Action
from agame.dialog import DialogOption
from .display import Display
if platform.system() in ("Linux", "Darwin", "Unix"):
@@ -14,5 +17,31 @@ class BasicDisplay(Display):
def line(self, line: str = ""):
print(line)
def input(self) -> str:
return input("> ")
def input(self, prompt: Optional[str] = None) -> str:
if prompt is None:
prompt = "> "
return input(prompt)
def wait_for_ack(self, prompt: Optional[str] = "Press enter to continue..."):
input(prompt or "")
def dialog_options(self, options: Sequence[DialogOption]) -> DialogOption:
valid_opts = range(1, len(options) + 1)
def print_opts():
for i, opt in enumerate(options):
self.line(f"{i}. {opt.text}")
print_opts()
while True:
got = self.input("? ")
try:
choice = int(got)
except ValueError:
print_opts()
continue
if choice in valid_opts:
break
index = choice - 1
selection = options[index]
return selection

View File

@@ -1,5 +1,10 @@
import abc
from typing import Optional
from typing import Optional, Sequence, Tuple, TYPE_CHECKING
from agame.action import Action
from agame.dialog import DialogOption
if TYPE_CHECKING:
from agame.game import Game
class Display(metaclass=abc.ABCMeta):
@@ -14,7 +19,7 @@ class Display(metaclass=abc.ABCMeta):
"Print a single line to the display."
@abc.abstractmethod
def input(self) -> str:
def input(self, prompt: Optional[str] = None) -> str:
"Get player input."
@abc.abstractmethod
@@ -28,5 +33,31 @@ class Display(metaclass=abc.ABCMeta):
user.
"""
def dialog_options(self, options: Sequence[DialogOption]) -> DialogOption:
"""
Displays, and gets, an answer from a list of dialog options.
Returns the selected DialogOption.next value and actions.
"""
valid_opts = range(1, len(options) + 1)
def print_opts():
for i, opt in enumerate(options):
self.line(f"{i}. {opt.text}")
print_opts()
while True:
got = self.input("? ")
try:
choice = int(got)
except ValueError:
print_opts()
continue
if choice in valid_opts:
break
index = choice - 1
selection = options[index]
return selection
def finish(self):
"Final cleanup code for this display."

View File

@@ -1,9 +1,14 @@
import sys
import termios
from typing import Optional, TextIO
from typing import Optional, Sequence, TextIO, Tuple, TYPE_CHECKING
from agame.color import colorize
from agame.action import Action
from agame.dialog import DialogOption
from .display import Display
if TYPE_CHECKING:
from agame.game import Game
ESC = "\u001b"
ANSI_MOVE_BOTTOM = f"{ESC}[999;0f"
@@ -45,10 +50,12 @@ class ANSIDisplay(Display):
self.stdout.write(line)
self.stdout.write("\n")
def input(self) -> str:
def input(self, prompt: Optional[str] = None) -> str:
if prompt is None:
prompt = self.input_prompt
try:
self.nocbreak()
return input(self.input_prompt)
return input(prompt)
finally:
self.cbreak()
@@ -59,6 +66,33 @@ class ANSIDisplay(Display):
self.line(prompt)
self.stdin.read(1)
def dialog_options(self, options: Sequence[DialogOption]) -> DialogOption:
valid_opts = range(1, len(options) + 1)
def print_opts():
for i, opt in zip(valid_opts, options):
# TODO - format/colorize this so it stands out
self.line(f"{i}. {opt.text}")
print_opts()
while True:
got = self.input("? ")
try:
choice = int(got)
except ValueError:
print_opts()
continue
if choice in valid_opts:
break
index = choice - 1
selection = options[index]
return selection
def finish(self):
# Clean up by restoring the old terminal state
fd = self.stdin.fileno()
termios.tcsetattr(fd, termios.TCSADRAIN, self.__old_attrs)
def cbreak(self):
fd = self.stdin.fileno()
new = termios.tcgetattr(fd)
@@ -71,11 +105,6 @@ class ANSIDisplay(Display):
new[3] |= termios.ECHO | termios.ICANON
termios.tcsetattr(fd, termios.TCSADRAIN, new)
def finish(self):
# Clean up by restoring the old terminal state
fd = self.stdin.fileno()
termios.tcsetattr(fd, termios.TCSADRAIN, self.__old_attrs)
def clear(self):
self.stdout.write(f"{ESC}[2J")