351 lines
7.9 KiB
Python
351 lines
7.9 KiB
Python
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 <word> - 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")
|