commit 8403013338ec3facd84e834a722568a0ff440d37 Author: Joscha Date: Tue Aug 7 11:00:42 2018 +0000 Move tom to separate repository diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fa9fbf3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +**/__pycache__ +yaboli +websockets +*.cookie +*.conf diff --git a/tom.conf.default b/tom.conf.default new file mode 100644 index 0000000..9218da5 --- /dev/null +++ b/tom.conf.default @@ -0,0 +1,9 @@ +[general] +nick = tom +cookiefile = tom.cookie + +[rooms] +# Format: +# room +# room=password +test diff --git a/tom.py b/tom.py new file mode 100644 index 0000000..9e249b2 --- /dev/null +++ b/tom.py @@ -0,0 +1,351 @@ +import asyncio +import configparser +import logging +import re + +import yaboli +from yaboli.utils import * + + +ELEMENTS = [ + ("H", "Hydrogen"), + ("He", "Helium"), + ("Li", "Lithium"), + ("Be", "Beryllium"), + ("B", "Boron"), + ("C", "Carbon"), + ("N", "Nitrogen"), + ("O", "Oxygen"), + ("F", "Fluorine"), + ("Ne", "Neon"), + ("Na", "Sodium"), + ("Mg", "Magnesium"), + ("Al", "Aluminium"), + ("Si", "Silicon"), + ("P", "Phosphorus"), + ("S", "Sulfur"), + ("Cl", "Chlorine"), + ("Ar", "Argon"), + ("K", "Potassium"), + ("Ca", "Calcium"), + ("Sc", "Scandium"), + ("Ti", "Titanium"), + ("V", "Vanadium"), + ("Cr", "Chromium"), + ("Mn", "Manganese"), + ("Fe", "Iron"), + ("Co", "Cobalt"), + ("Ni", "Nickel"), + ("Cu", "Copper"), + ("Zn", "Zinc"), + ("Ga", "Gallium"), + ("Ge", "Germanium"), + ("As", "Arsenic"), + ("Se", "Selenium"), + ("Br", "Bromine"), + ("Kr", "Krypton"), + ("Rb", "Rubidium"), + ("Sr", "Strontium"), + ("Y", "Yttrium"), + ("Zr", "Zirconium"), + ("Nb", "Niobium"), + ("Mo", "Molybdenum"), + ("Tc", "Technetium"), + ("Ru", "Ruthenium"), + ("Rh", "Rhodium"), + ("Pd", "Palladium"), + ("Ag", "Silver"), + ("Cd", "Cadmium"), + ("In", "Indium"), + ("Sn", "Tin"), + ("Sb", "Antimony"), + ("Te", "Tellurium"), + ("I", "Iodine"), + ("Xe", "Xenon"), + ("Cs", "Caesium"), + ("Ba", "Barium"), + ("La", "Lanthanum"), + ("Ce", "Cerium"), + ("Pr", "Praseodymium"), + ("Nd", "Neodymium"), + ("Pm", "Promethium"), + ("Sm", "Samarium"), + ("Eu", "Europium"), + ("Gd", "Gadolinium"), + ("Tb", "Terbium"), + ("Dy", "Dysprosium"), + ("Ho", "Holmium"), + ("Er", "Erbium"), + ("Tm", "Thulium"), + ("Yb", "Ytterbium"), + ("Lu", "Lutetium"), + ("Hf", "Hafnium"), + ("Ta", "Tantalum"), + ("W", "Tungsten"), + ("Re", "Rhenium"), + ("Os", "Osmium"), + ("Ir", "Iridium"), + ("Pt", "Platinum"), + ("Au", "Gold"), + ("Hg", "Mercury"), + ("Tl", "Thallium"), + ("Pb", "Lead"), + ("Bi", "Bismuth"), + ("Po", "Polonium"), + ("At", "Astatine"), + ("Rn", "Radon"), + ("Fr", "Francium"), + ("Ra", "Radium"), + ("Ac", "Actinium"), + ("Th", "Thorium"), + ("Pa", "Protactinium"), + ("U", "Uranium"), + ("Np", "Neptunium"), + ("Pu", "Plutonium"), + ("Am", "Americium"), + ("Cm", "Curium"), + ("Bk", "Berkelium"), + ("Cf", "Californium"), + ("Es", "Einsteinium"), + ("Fm", "Fermium"), + ("Md", "Mendelevium"), + ("No", "Nobelium"), + ("Lr", "Lawrencium"), + ("Rf", "Rutherfordium"), + ("Db", "Dubnium"), + ("Sg", "Seaborgium"), + ("Bh", "Bohrium"), + ("Hs", "Hassium"), + ("Mt", "Meitnerium"), + ("Ds", "Darmstadtium"), + ("Rg", "Roentgenium"), + ("Cn", "Copernicium"), + ("Nh", "Nihonium"), + ("Fl", "Flerovium"), + ("Mc", "Moscovium"), + ("Lv", "Livermorium"), + ("Ts", "Tennessine"), + ("Og", "Oganesson"), + # Isotopes, but hey, they make the words look better + ("D", "Deuterium"), + ("T", "Tritium"), + # Non-elements and eastereggs + ("E", "Euphorium"), + ("Xyzzy", "Plugh"), + ("Plugh", "Xyzzy"), + ("hunter2", "*******"), +] +ELEMDICT = {symbol.lower(): name for (symbol, name) in ELEMENTS} +ELEMLENGTHS = set(len(symbol) for (symbol, _) in ELEMENTS) + +class Node: + def __init__(self, stump): + self.stump = stump + self.edges = [] + +class Edge: + LETTER = 0 + ELEMENT = 1 + + def __init__(self, start, end, weight, edgetype, text): + self.start = start + self.end = end + self.weight = weight + self.edgetype = edgetype + self.text = text + +def elem_prefixes(text): + elems = [] + for i in ELEMLENGTHS: + if len(text) < i: continue + + start, rest = text[:i], text[i:] + elem = ELEMDICT.get(start.lower(), None) + if elem: + elems.append((elem, rest)) + + return elems + +def create_graph(text): + graph = {} + stumps = [text[i:] for i in range(len(text))] + + # goal node + graph[""] = Node("") + + # creating all the nodes + for stump in stumps: + graph[stump] = Node(stump) + + # creating single-letter links + weight = len(text) + for start in stumps: + char, end = start[:1], start[1:] + nstart = graph.get(start) + nend = graph.get(end) + edge = Edge(nstart, nend, weight, Edge.LETTER, char) + nstart.edges.append(edge) + + # creating element links + for stump in stumps: + elems = elem_prefixes(stump) + for (elem, rest) in elems: + nstart = graph.get(stump) + nend = graph.get(rest) + edge = Edge(nstart, nend, 1, Edge.ELEMENT, elem) + nstart.edges.append(edge) + + return graph + +def smallest(graph, nodes): + smallest = None + for node in nodes: + if not smallest or node.length < smallest.length: + smallest = node + return smallest + +def dijkstra(graph, start, end): + unvisited = set() + visited = set() + + nstart = graph.get(start) + nstart.step = None + nstart.length = 0 + for edge in nstart.edges: + node = edge.end + unvisited.add(node) + node.step = edge + node.length = edge.weight + visited.add(nstart) + + while unvisited: + small = smallest(graph, unvisited) + unvisited.remove(small) + visited.add(small) + + if small.stump == "": + break + + for edge in small.edges: + node = edge.end + if node in visited: continue + length = small.length + edge.weight + if node in unvisited: + if length < node.length: + node.step = edge + node.length = length + else: + unvisited.add(node) + node.step = edge + node.length = length + + path = [] + node = graph.get(end) + while node.step: + path.append(node.step) + node = node.step.start + + return reversed(path) + +def format_path(path): + parts = [] + part = "" + + for edge in path: + if edge.edgetype == Edge.LETTER: + part += edge.text + else: + if part: + parts.append(part) + part = "" + parts.append(edge.text) + + if part: + parts.append(part) + + return "-".join(parts) + +def shortest_path(text): + graph = create_graph(text) + path = dijkstra(graph, text, "") + return path + +class Tom: + SHORT_DESCRIPTION = "spell a word using chemical elements, if possible" + DESCRIPTION = ( + "'tom' attempts to spell a word using the symbols of the periodic table." + " For example, 'Hi' becomes 'Hydrogen Iodine'.\n" + "Because of this, only the letters a-z and A-Z may be used in a word.\n" + "'tom' uses en.wikipedia.org/wiki/List_of_chemical_elements for the element names.\n" + ) + COMMANDS = "!tom - attempts to spell the word using chemical elements\n" + AUTHOR = "Created by @Garmy using github.com/Garmelon/yaboli\n" + CREDITS = "Inspired by Tomsci. Algorithm based on a suggestion by Xyzzy.\n" + + ALLOWED_CHARS = r"a-zA-Z*\w" + ELEM_RE = "([" + ALLOWED_CHARS + "]*)([^" + ALLOWED_CHARS + "]*)" + + @yaboli.command("tom") + async def command_tom(self, room, message, argstr): + args = [] + while argstr: + match = re.match(self.ELEM_RE, argstr) + if not match: break + + args.append((match.group(1), match.group(2))) + argstr = argstr[match.end():] + + spellings = [] + for fst, snd in args: + if fst: + spelling = format_path(shortest_path(fst)) + snd + else: + spelling = snd + spellings.append(spelling) + + text = "".join(spellings) + await room.send(text, message.mid) + +class TomBot(yaboli.Bot): + SHORT_HELP = Tom.SHORT_DESCRIPTION + LONG_HELP = Tom.DESCRIPTION + Tom.COMMANDS + Tom.AUTHOR + Tom.CREDITS + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.tom = Tom() + + async def on_command_specific(self, room, message, command, nick, argstr): + if similar(nick, room.session.nick) and not argstr: + await self.botrulez_ping(room, message, command) + await self.botrulez_help(room, message, command, text=self.LONG_HELP) + await self.botrulez_uptime(room, message, command) + await self.botrulez_kill(room, message, command) + await self.botrulez_restart(room, message, command) + + async def on_command_general(self, room, message, command, argstr): + if not argstr: + await self.botrulez_ping(room, message, command) + await self.botrulez_help(room, message, command, text=self.SHORT_HELP) + + await self.tom.command_tom(room, message, command, argstr) + +def main(configfile): + logging.basicConfig(level=logging.INFO) + + config = configparser.ConfigParser(allow_no_value=True) + config.read(configfile) + + nick = config.get("general", "nick") + cookiefile = config.get("general", "cookiefile", fallback=None) + + bot = TomBot(nick, cookiefile=cookiefile) + + for room, password in config.items("rooms"): + if not password: + password = None + bot.join_room(room, password=password) + + asyncio.get_event_loop().run_forever() + +if __name__ == "__main__": + main("tom.conf")