aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/examples/plugins/nivis.❄
diff options
context:
space:
mode:
Diffstat (limited to 'examples/plugins/nivis.❄')
-rw-r--r--examples/plugins/nivis.❄295
1 files changed, 295 insertions, 0 deletions
diff --git a/examples/plugins/nivis.❄ b/examples/plugins/nivis.❄
new file mode 100644
index 0000000..783fdb1
--- /dev/null
+++ b/examples/plugins/nivis.❄
@@ -0,0 +1,295 @@
+""" SPI - Simple Pascal Interpreter """
+
+###############################################################################
+# #
+# LEXER #
+# #
+###############################################################################
+
+# Token types
+#
+# EOF (end-of-file) token is used to indicate that
+# there is no more input left for lexical analysis
+from iamai import Plugin
+from HydroRoll.utils import HydroDice
+
+INTEGER, PLUS, MINUS, MUL, DIV, LPAREN, RPAREN, EOF = (
+ "INTEGER",
+ "PLUS",
+ "MINUS",
+ "MUL",
+ "DIV",
+ "(",
+ ")",
+ "EOF",
+)
+
+DICE = "DICE"
+
+
+class Token(object):
+ """A single token from the lexer."""
+
+ def __init__(self, _type, _value):
+ self.type = _type
+ self.value = _value
+
+ def __str__(self):
+ """String representation of the class instance.
+
+ Examples:
+ Token(INTEGER, 3)
+ Token(PLUS, '+')
+ Token(MUL, '*')
+ """
+ return f"Token({self.type}, {repr(self.value)})"
+
+ def __repr__(self):
+ return self.__str__()
+
+
+class Lexer(object):
+ """A lexer for the Psi language."""
+
+ def __init__(self, text):
+ # client string input, e.g. "4 + 2 * 3 - 6 / 2"
+ self.text = text
+ # self.pos is an index into self.text
+ self.pos = 0
+ self.current_char = self.text[self.pos]
+
+ def error(self):
+ """Raise an exception at the current position."""
+ raise ValueError("Invalid character")
+
+ def advance(self):
+ """Advance the `pos` pointer and set the `current_char` variable."""
+ self.pos += 1
+ if self.pos > len(self.text) - 1:
+ self.current_char = None # Indicates end of input
+ else:
+ self.current_char = self.text[self.pos]
+
+ def skip_whitespace(self):
+ while self.current_char is not None and self.current_char.isspace():
+ self.advance()
+
+ def integer(self):
+ """Return a (multidigit) integer consumed from the input."""
+ result = ""
+ while self.current_char is not None and self.current_char.isdigit():
+ result += self.current_char
+ self.advance()
+ return int(result)
+
+ def get_next_token(self):
+ """Lexical analyzer (also known as scanner or tokenizer)"""
+ while self.current_char is not None:
+ if self.current_char.isspace():
+ self.skip_whitespace()
+ continue
+
+ token_type = {
+ "+": PLUS,
+ "-": MINUS,
+ "d": DICE,
+ "*": MUL,
+ "/": DIV,
+ "(": LPAREN,
+ ")": RPAREN,
+ }.get(self.current_char)
+
+ if token_type:
+ self.advance()
+ return Token(token_type, self.current_char)
+
+ if self.current_char.isdigit():
+ return Token(INTEGER, self.integer())
+
+ self.error()
+
+ return Token(EOF, None)
+
+
+###############################################################################
+# #
+# PARSER #
+# #
+###############################################################################
+
+
+class AST(object):
+ pass
+
+
+class BinOp(AST):
+ def __init__(self, left, op, right):
+ self.left = left
+ self.token = self.op = op
+ self.right = right
+
+
+class Num(AST):
+ def __init__(self, token):
+ self.token = token
+ self.value = token.value
+
+
+class UnaryOp(AST):
+ def __init__(self, op, expr):
+ self.token = self.op = op
+ self.expr = expr
+
+
+class Parser(object):
+ def __init__(self, lexer):
+ self.lexer = lexer
+ # set current token to the first token taken from the input
+ self.current_token = self.lexer.get_next_token()
+
+ def error(self):
+ raise Exception("Invalid syntax")
+
+ def eat(self, token_type):
+ # compare the current token type with the passed token
+ # type and if they match then "eat" the current token
+ # and assign the next token to the self.current_token,
+ # otherwise raise an exception.
+ if self.current_token.type == token_type:
+ self.current_token = self.lexer.get_next_token()
+ else:
+ self.error()
+
+ def factor(self):
+ """factor : (PLUS | MINUS | DICE) factor | INTEGER | LPAREN expr RPAREN"""
+ token = self.current_token
+ if token.type == PLUS:
+ self.eat(PLUS)
+ node = UnaryOp(token, self.factor())
+ return node
+ elif token.type == MINUS:
+ self.eat(MINUS)
+ node = UnaryOp(token, self.factor())
+ return node
+ elif token.type == DICE:
+ self.eat(DICE)
+ left = Num(Token(INTEGER, 1)) # 默认骰子个数为1
+ right = self.factor()
+ node = BinOp(left, token, right)
+ return node
+ elif token.type == INTEGER:
+ self.eat(INTEGER)
+ return Num(token)
+ elif token.type == LPAREN:
+ self.eat(LPAREN)
+ node = self.expr()
+ self.eat(RPAREN)
+ return node
+
+ def term(self):
+ """term : factor ((MUL | DIV) factor)*"""
+ node = self.factor()
+
+ while self.current_token.type in (MUL, DIV):
+ token = self.current_token
+ if token.type == MUL:
+ self.eat(MUL)
+ elif token.type == DIV:
+ self.eat(DIV)
+
+ node = BinOp(left=node, op=token, right=self.factor())
+
+ return node
+
+ def expr(self):
+ """
+ expr : term ((PLUS | MINUS) term)*
+ term : factor ((MUL | DIV) factor)*
+ factor : (PLUS | MINUS) factor | INTEGER | LPAREN expr RPAREN
+ """
+ node = self.term()
+
+ while self.current_token.type in (PLUS, MINUS):
+ token = self.current_token
+ if token.type == PLUS:
+ self.eat(PLUS)
+ elif token.type == MINUS:
+ self.eat(MINUS)
+
+ node = BinOp(left=node, op=token, right=self.term())
+
+ return node
+
+ def parse(self):
+ node = self.expr()
+ if self.current_token.type != EOF:
+ self.error()
+ return node
+
+
+###############################################################################
+# #
+# INTERPRETER #
+# #
+###############################################################################
+
+
+class NodeVisitor(object):
+ def visit(self, node):
+ method_name = "visit_" + type(node).__name__
+ visitor = getattr(self, method_name, self.generic_visit)
+ return visitor(node)
+
+ def generic_visit(self, node):
+ raise Exception("No visit_{} method".format(type(node).__name__))
+
+
+class Interpreter(NodeVisitor):
+ def __init__(self, parser):
+ self.parser = parser
+
+ def visit_BinOp(self, node):
+ if node.op.type == PLUS:
+ return self.visit(node.left) + self.visit(node.right)
+ elif node.op.type == MINUS:
+ return self.visit(node.left) - self.visit(node.right)
+ elif node.op.type == DICE:
+ return int(
+ HydroDice(1).roll_dice(
+ _counts=self.visit(node.left),
+ _sides=self.visit(node.right),
+ streamline=True,
+ )
+ )
+ elif node.op.type == MUL:
+ return self.visit(node.left) * self.visit(node.right)
+ elif node.op.type == DIV:
+ return self.visit(node.left) // self.visit(node.right)
+
+ def visit_Num(self, node):
+ return node.value
+
+ def visit_UnaryOp(self, node):
+ op = node.op.type
+ if op == PLUS:
+ return +self.visit(node.expr)
+ elif op == MINUS:
+ return -self.visit(node.expr)
+
+ def interpret(self):
+ tree = self.parser.parse()
+ if tree is None:
+ return ""
+ return self.visit(tree)
+
+
+class Psi(Plugin):
+ async def handle(self) -> None:
+ lexer = Lexer(self.event.message.get_plain_text()[4:])
+ parser = Parser(lexer)
+ interpreter = Interpreter(parser)
+ result = interpreter.interpret()
+ await self.event.reply(str(result))
+
+ async def rule(self) -> bool:
+ return self.event.type == "message" and self.event.message.startswith(".psi")