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:
@@ -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
40
agame/dialog.py
Normal 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.",
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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."
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user