zmbc/shakespearelang

View on GitHub
shakespearelang/_operation.py

Summary

Maintainability
A
1 hr
Test Coverage
A
90%
from ._utils import normalize_name
from ._expression import expression_from_ast
from .errors import ShakespeareRuntimeError, ShakespeareParseError
from tatsu.ast import AST


class Operation:
    def __init__(self, ast_node: AST):
        self.ast_node = ast_node
        self._setup(ast_node)

    def _setup(self, ast_node):
        pass

    def run(self, state, settings):
        try:
            self._run_logic(state, settings)
        except ShakespeareRuntimeError as exc:
            if not exc.parseinfo:
                exc.parseinfo = self.ast_node.parseinfo
            raise exc

    def _run_logic(self, state, settings):
        pass


class Entrance(Operation):
    def _setup(self, ast_node: AST):
        self.characters = [normalize_name(c) for c in ast_node.characters]

    def _run_logic(self, state, settings):
        if settings.output_style in ["verbose", "debug"]:
            print(f"Enter {', '.join(self.characters)}")
        state.enter_characters(self.characters)


class Exit(Operation):
    def _setup(self, ast_node: AST):
        self.character = normalize_name(ast_node.character)

    def _run_logic(self, state, settings):
        if settings.output_style in ["verbose", "debug"]:
            print(f"Exit {self.character}")
        state.exit_character(self.character)


class Exeunt(Operation):
    def _setup(self, ast_node: AST):
        if ast_node.characters:
            self.characters = [normalize_name(c) for c in ast_node.characters]
        else:
            self.characters = None

    def _run_logic(self, state, settings):
        if self.characters is not None:
            if settings.output_style in ["verbose", "debug"]:
                print(f"Exeunt {', '.join(self.characters)}")
            state.exeunt_characters(self.characters)
        else:
            if settings.output_style in ["verbose", "debug"]:
                print("Exeunt all")
            state.exeunt_all()


class Breakpoint(Operation):
    pass


class SentenceOperation(Operation):
    def __init__(self, ast_node: AST, character: str):
        self.ast_node = ast_node
        self.op_ast_node = ast_node.operation
        self.character = normalize_name(character)
        self.has_condition = ast_node.condition is not None
        if self.has_condition:
            self.condition_type_positive = (
                ast_node.condition.parseinfo.rule == "positive_if"
            )
        else:
            self.condition_type_positive = None
        self._setup()

    def _setup(self):
        pass

    def run(self, state, settings):
        state.assert_character_on_stage(self.character)

        if self.has_condition and self.condition_type_positive != state.global_boolean:
            if settings.output_style in ["verbose", "debug"]:
                print(
                    f"Not executing conditional {type(self).__name__.lower()}, global boolean is {state.global_boolean}"
                )
        else:
            try:
                self._run_logic(state, settings)
            except ShakespeareRuntimeError as exc:
                if not exc.parseinfo:
                    exc.parseinfo = self.ast_node.parseinfo
                raise exc


class Question(SentenceOperation):
    _COMPARATIVE_TYPE_HANDLERS = {
        "positive_comparative": lambda a, b: a > b,
        "negative_comparative": lambda a, b: a < b,
        "neutral_comparative": lambda a, b: a == b,
    }

    def _setup(self):
        self.first_value = expression_from_ast(
            self.op_ast_node.first_value, self.character
        )
        self.second_value = expression_from_ast(
            self.op_ast_node.second_value, self.character
        )
        comparative_rule = self.op_ast_node.comparative.parseinfo.rule
        if comparative_rule not in self._COMPARATIVE_TYPE_HANDLERS:
            raise ShakespeareRuntimeError(
                f"Unknown comparative type: {comparative_rule}"
            )
        self.comparison = self._COMPARATIVE_TYPE_HANDLERS[comparative_rule]

    def _run_logic(self, state, settings):
        result = self._evaluate(state)

        if settings.output_style in ["verbose", "debug"]:
            print(f"Setting global boolean to {result}")

        state.global_boolean = result

    def _evaluate(self, state) -> bool:
        return self.comparison(
            self.first_value.evaluate(state), self.second_value.evaluate(state)
        )


class Assignment(SentenceOperation):
    def _setup(self):
        self.value = expression_from_ast(self.op_ast_node.value, self.character)

    def _run_logic(self, state, settings):
        character_opposite = state.character_opposite(self.character)
        value = self.value.evaluate(state)
        state.character_by_name(character_opposite).value = value

        if settings.output_style in ["verbose", "debug"]:
            print(f"{character_opposite} set to {value}")


class Input(SentenceOperation):
    def _setup(self):
        self.input_type = "number" if self.op_ast_node.input_number else "char"

    def _run_logic(self, state, settings):
        character_to_set = state.character_opposite(self.character)
        if self.input_type == "number":
            value = settings.input_manager.consume_numeric_input()
        else:
            value = settings.input_manager.consume_character_input()

        if settings.output_style in ["verbose", "debug"]:
            print(f"Setting {character_to_set} to input value {repr(value)}")

        state.character_by_name(character_to_set).value = value


class Output(SentenceOperation):
    def _setup(self):
        self.output_type = "number" if self.op_ast_node.output_number else "char"

    def _run_logic(self, state, settings):
        character_to_output = state.character_opposite(self.character)
        value = state.character_by_name(character_to_output).value
        if settings.output_style in ["verbose", "debug"]:
            print(f"Outputting {character_to_output}")
        if self.output_type == "number":
            settings.output_manager.output_number(value)
        else:
            settings.output_manager.output_character(value)


class Push(SentenceOperation):
    def _setup(self):
        self.value = expression_from_ast(self.op_ast_node.value, self.character)

    def _run_logic(self, state, settings):
        pushing_character = state.character_opposite(self.character)
        value = self.value.evaluate(state)
        state.character_by_name(pushing_character).push(value)

        if settings.output_style in ["verbose", "debug"]:
            print(f"{pushing_character} pushed {value}")


class Pop(SentenceOperation):
    def _run_logic(self, state, settings):
        popping_character = state.character_opposite(self.character)
        state.character_by_name(popping_character).pop()

        if settings.output_style in ["verbose", "debug"]:
            print(f"Popping stack of {popping_character}")


class Goto(SentenceOperation):
    def _setup(self):
        self.destination = self.op_ast_node.destination.value

    def run(self, state, interpreter, play, settings):
        state.assert_character_on_stage(self.character)

        if self.has_condition and self.condition_type_positive != state.global_boolean:
            if settings.output_style in ["verbose", "debug"]:
                print(
                    f"Not jumping to Scene {self.destination} because global boolean is {state.global_boolean}"
                )
            return

        if settings.output_style in ["verbose", "debug"]:
            print(f"Jumping to Scene {self.destination}")
        act = play.get_act(interpreter.current_position)
        if self.destination not in play.scene_indices[act]:
            raise ShakespeareRuntimeError(f"Scene {self.destination} does not exist.")
        new_position = play.scene_indices[act][self.destination]
        interpreter.current_position = new_position


_OPERATIONS_CONSTRUCTORS = {
    "entrance": Entrance,
    "exit": Exit,
    "exeunt": Exeunt,
    "breakpoint": Breakpoint,
    "question": Question,
    "assignment": Assignment,
    "input": Input,
    "output": Output,
    "push": Push,
    "pop": Pop,
    "goto": Goto,
}


def operations_from_event(event: AST):
    rule = event.parseinfo.rule
    if rule == "line":
        return [operation_from_sentence(s, event.character) for s in event.contents]
    else:
        return [_OPERATIONS_CONSTRUCTORS[rule](event)]


def operation_from_sentence(sentence: AST, character: str):
    sentence_operation_rule = sentence.operation.parseinfo.rule
    return _OPERATIONS_CONSTRUCTORS[sentence_operation_rule](sentence, character)