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 dataclasses
import time import time
from typing import Any, Optional, Sequence, Union, TYPE_CHECKING from typing import Any, Mapping, Optional, Sequence, Union, TYPE_CHECKING
import enum import enum
if TYPE_CHECKING: if TYPE_CHECKING:
from agame.game import Game from agame.game import Game
from agame.dialog import DialogOption
__all__ = ( __all__ = (
@@ -24,6 +25,7 @@ __all__ = (
"SetVarAction", "SetVarAction",
"RevealItemAction", "RevealItemAction",
"UnrevealItemAction", "UnrevealItemAction",
"DialogAction",
) )
@@ -441,3 +443,32 @@ class UnrevealItemAction(Action):
self.item_id in game.room.items self.item_id in game.room.items
), f"item id {self.item_id} does not exist in the current room {game.room.id}" ), f"item id {self.item_id} does not exist in the current room {game.room.id}"
game.room.items[self.item_id].revealed = False 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 import platform
from typing import Optional, Sequence, Tuple
from agame.action import Action
from agame.dialog import DialogOption
from .display import Display from .display import Display
if platform.system() in ("Linux", "Darwin", "Unix"): if platform.system() in ("Linux", "Darwin", "Unix"):
@@ -14,5 +17,31 @@ class BasicDisplay(Display):
def line(self, line: str = ""): def line(self, line: str = ""):
print(line) print(line)
def input(self) -> str: def input(self, prompt: Optional[str] = None) -> str:
return input("> ") 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 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): class Display(metaclass=abc.ABCMeta):
@@ -14,7 +19,7 @@ class Display(metaclass=abc.ABCMeta):
"Print a single line to the display." "Print a single line to the display."
@abc.abstractmethod @abc.abstractmethod
def input(self) -> str: def input(self, prompt: Optional[str] = None) -> str:
"Get player input." "Get player input."
@abc.abstractmethod @abc.abstractmethod
@@ -28,5 +33,31 @@ class Display(metaclass=abc.ABCMeta):
user. 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): def finish(self):
"Final cleanup code for this display." "Final cleanup code for this display."

View File

@@ -1,9 +1,14 @@
import sys import sys
import termios import termios
from typing import Optional, TextIO from typing import Optional, Sequence, TextIO, Tuple, TYPE_CHECKING
from agame.color import colorize from agame.color import colorize
from agame.action import Action
from agame.dialog import DialogOption
from .display import Display from .display import Display
if TYPE_CHECKING:
from agame.game import Game
ESC = "\u001b" ESC = "\u001b"
ANSI_MOVE_BOTTOM = f"{ESC}[999;0f" ANSI_MOVE_BOTTOM = f"{ESC}[999;0f"
@@ -45,10 +50,12 @@ class ANSIDisplay(Display):
self.stdout.write(line) self.stdout.write(line)
self.stdout.write("\n") 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: try:
self.nocbreak() self.nocbreak()
return input(self.input_prompt) return input(prompt)
finally: finally:
self.cbreak() self.cbreak()
@@ -59,6 +66,33 @@ class ANSIDisplay(Display):
self.line(prompt) self.line(prompt)
self.stdin.read(1) 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): def cbreak(self):
fd = self.stdin.fileno() fd = self.stdin.fileno()
new = termios.tcgetattr(fd) new = termios.tcgetattr(fd)
@@ -71,11 +105,6 @@ class ANSIDisplay(Display):
new[3] |= termios.ECHO | termios.ICANON new[3] |= termios.ECHO | termios.ICANON
termios.tcsetattr(fd, termios.TCSADRAIN, new) 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): def clear(self):
self.stdout.write(f"{ESC}[2J") self.stdout.write(f"{ESC}[2J")