#!/usr/bin/python3 # -*- coding: utf-8 -*- """Lienzo para multiples usuarios. Esto es un lienzo para hacer una ficción interactiva *mínima* como un Wiki. Los objetos del juego participan en una jerarquía de contención, muchas veces siendo contenido dentro de algún contenedor, tal como una habitación o el jugador; y tienen nombres y descripciones. El mundo es transparentemente persistente a través de pickle. Los comandos incluyen: - mirar - ver objeto - norte, sur, este, oeste, arriba, bajar (abreviaciones n, s, e, o, a, b) - renombrar objeto nuevonombre - describir objeto como larga descripción - crear objeto - tomar objeto - inventario - soltar objeto - cavar {norte, sur, etc.} ?a lugar? Cavar sin especificar un destino crea un nuevo lugar. Así podés crear tu mundo habitación por habitación y describir sus objetos. Lo que no podés hacer todavía es destruir o programar comportamientos. """ # Para hacer: # - agregar help # - agregar alguna suerte de undo # - quizás algún tipo de STM? tal vez probar eso aparte # - limitar retries de contraseñas # - limpiar las tareas mejor en ^C y capaz que anunciar a los jugadores también # - loguear los comandos # - limitar floodear # - únicamente fsyncear cada 30 segundos o menos # - arreglar género de sustantivos (“Dumur crea un naranja”, “Dumur # toma el naranja.”, “Dumur suelta un naranja.”, “Riayhur mira el # naranja.”, “No hay ningún “naranja” acá.”, “Riayhur crea un # problemas acá.”, “Ahora problemas ya no es “problemas” sino # “quilombos”.”) # - arreglar “el arriba” y “el bajar” (y “cava una salida al bajar” y # “se va al bajar”) # - generar descripciones aleatorias para nuevas habitaciones # - permitir actualizar el código sin perder las conexiones # - usar difflib para adivinar verbos # - no requerir tildes import asyncio, re, os, traceback, textwrap, time, random, pickle def y_list(words): words = list(words) if len(words) == 1: return words[0] if len(words) == 2: return ' y '.join(words) return ', '.join(words[:-1] + ['y ' + words[-1]]) class Thing(object): def __init__(self, name, description, container=None): self.name = name self.aliases = set() self.description = description self.contents = [] self.container = container if container is not None: container.add(self) is_takable = True def is_a(self, name): return name == self.name or name in self.aliases def find(self, name): for thing in self.contents: if thing.is_a(name): return thing def move_to(self, container): if self.container: self.container.remove(self) self.container = container container.add(self) def remove(self, item): self.contents.remove(item) def add(self, item): self.contents.append(item) def describe(self): content_string = '' if self.contents: content_string = ('\n\n' + self.name + ' contiene: '.format(self.name) + y_list(obj.name for obj in self.contents) + '.') return '{}\n\n{}{}'.format(self.name, self.description, content_string) def tell(self, message, exclude=None): for item in self.contents: if item != exclude: item.tell(message) def find_thing(player, room, name): return player.find(name) or room.find(name) or (room if room.is_a(name) else None) def find_thing_vocally(player, room, name): thing = find_thing(player, room, name) if not thing: player.tell('No hay ningún “{}” a la vista.'.format(name)) return thing class Room(Thing): def __init__(self, name, description, container): super(Room, self).__init__(name, description, container) self.exits = {} self.aliases.update(['aca', 'acá', 'aquí', 'habitación']) is_takable = False def go(self, player, direction): dest = self.exits.get(direction) if not dest: return False self.tell('{} se va al {}.'.format(player.name, direction), exclude=player) player.move_to(dest) dest.tell('{} entra.'.format(player.name), exclude=player) player.tell(dest.describe()) return True def describe(self): desc = super(Room, self).describe() if self.exits: desc += '\n\nHay salida hacia el {}.'.format(y_list(self.exits.keys())) return desc connections = {} class Player(Thing): def __init__(self, name, description, truename, container): super(Player, self).__init__(name, description, container) self.truename = truename self.asleep = True is_takable = False def tell(self, text): if self in connections: connections[self].send(wrap(text)) def put_to_sleep(self): self.container.tell("{} se pone a roncar.".format(self.name)) self.asleep = True def wake_up(self): self.asleep = False self.container.tell("{} se despierta.".format(self.name), exclude=self) def describe(self): desc = super(Player, self).describe() if self.asleep: desc += '\n\nEstá roncando.' return desc def wrap(text): paragraphs = text.split('\n\n') return '\n\n'.join('\n'.join(textwrap.wrap(para)) for para in paragraphs) verbs = {} def verb(fun): verbs[fun.__name__] = fun return fun @verb def mirar(player, world, args): if args: thing = find_thing_vocally(player, player.container, args) else: thing = player.container if not thing: return player.tell(thing.describe()) player.container.tell('{} mira el {}.'.format(player.name, thing.name), exclude=player) verbs['ver'] = mirar @verb def renombrar(player, world, args): name, newname = args.split(None, 1) thing = find_thing_vocally(player, player.container, name) if not thing: return if not newname: player.tell('Qué querés llamar el {}?'.format(thing.name)) return oldname = thing.name thing.name = newname player.container.tell('Ahora {} ya no es “{}” sino “{}”.'.format(name, oldname, newname)) verbs['llamar'] = renombrar @verb def cavar(player, world, args): direction = args.split()[0] if direction not in directions: player.tell('No sé dónde está {}; conozco los sentidos {}.'.format(direction, y_list(directions.keys()))) return d2 = directions[direction] dest = args[len(direction):].strip() if dest == '': new_room = Room('Otro vacío', 'Estás en otra habitación sin características particulares.', container=world) else: if not dest.startswith('a '): player.tell('No entiendo "{}" ({}, {}); podés escribir algo como “cavar norte a pasillo.”'.format(dest, direction, args)) return dest = dest[2:] new_room = world.find(dest) # The contents of the world are the rooms. if not new_room: player.tell('No sé qué es "{}".'.format(dest)) return old_room = player.container old_room.exits[d2] = new_room reverse = antonym[d2] if reverse not in new_room.exits: new_room.exits[reverse] = old_room old_room.tell('{} cava una salida al {}.'.format(player.name, d2), exclude=player) old_room.go(player, d2) @verb def describir(player, world, args): try: name, como, desc = args.split(None, 2) except ValueError: return explain_describir(player) if como != 'como': return explain_describir(player) thing = find_thing_vocally(player, player.container, name) if not thing: return thing.description = desc player.tell("Descripción de “{}” cambiado.".format(thing.name)) def explain_describir(player): player.tell('No entiendo; podés decir cosas como “describir aca como Hay un atardecer visible al oeste.”') @verb def crear(player, world, args): thing = Thing(args, "No ves nada muy interesante. Podés escribir “describir {} como Algo muy interesante.”".format(args), container=player.container) player.tell("Creaste un {}.".format(args)) player.container.tell("{} crea un {} acá.".format(player.name, args), exclude=player) @verb def tomar(player, world, args): if not args: player.tell("Qué querés tomar?") return thing = player.container.find(args) if not thing: player.tell("No hay ningún “{}” acá.".format(args)) return if not thing.is_takable: player.tell("{} no se puede tomar.".format(thing.name)) return thing.container.tell('{} toma el {}.'.format(player.name, thing.name), exclude=player) thing.move_to(player) player.tell("Tomado.") @verb def inventario(player, world, args): if not player.contents: return player.tell("No tenés un carajo.") player.tell("Tenés:") for item in player.contents: player.tell("- {}".format(item.name)) player.tell("Eso es todo.") @verb def soltar(player, world, args): if not args: player.tell("Qué querés soltar?") return thing = player.find(args) if not thing: player.tell("No tenés ningún “{}”.".format(args)) return thing.move_to(player.container) player.tell("Soltado.") player.container.tell('{} suelta un {}.'.format(player.name, thing.name), exclude=player) verbs['dejar'] = soltar @verb def decir(player, world, args): player.container.tell('{} dice, “{}”.'.format(player.name, args)) @verb def hacer(player, world, args): player.container.tell('{} {}.'.format(player.name, args)) directions = {'n': 'norte', 's': 'sur', 'o': 'oeste', 'e': 'este', 'a': 'arriba', 'b': 'bajar', 'abajo': 'bajar', 'subir': 'arriba'} for v in list(directions.values()): directions[v] = v antonym = {} for k, v in [('norte', 'sur'), ('este', 'oeste'), ('arriba', 'bajar')]: antonym[k] = v antonym[v] = k def ir_verb(direction): def ir_verb_fun(player, world, args): d2 = directions[direction] room = player.container if not room.go(player, d2): player.tell("No hay salida de acá hacia el {}; podés “cavar {}”.".format(d2, d2)) return ir_verb_fun for v in directions: verbs[v] = ir_verb(v) class World(Thing): def __init__(self, *args, **kwargs): super(World, self).__init__(*args, **kwargs) self.avatars = [] is_takable = False def basic_world(): world = World("El mundo", "El mundo entero, donde se encuentran todos los lugares.") room = Room('Sipapu', 'Estás en un lugar borroso, sin mucha forma visible, con un pozo.', container=world) return world spsyllables = [C + V + coda for C in ['b', 'ch', 'd', 'f', 'h', 'j', 'k', 'l', 'm', 'n', 'p', 'r', 's', 't'] for V in ['a', 'ia', 'e', 'ie', 'i', 'o', 'ue', 'u'] for coda in ['', 's', 'y', 'r']] def new_name(n): sr = random.SystemRandom() return ''.join(sr.choice(spsyllables) for _ in range(n)).title() def new_truename(): "Generate a secret truename with some 43 bits of security." return ' '.join([new_name(2), new_name(3)]) def new_avatar(room): player = Player(new_name(2), 'Alguna suerte de ser vivo.', truename=new_truename(), container=room) player.aliases.update(['yo', 'mi', 'mí']) return player world_filename = 'mundolmu.pck' def save_world(world): tmpname = 'tmp.' + world_filename with open(tmpname, 'wb') as f: pickle.dump(world, f) f.flush() os.fsync(f.fileno()) # hey, it’s 2020, this is okay, right? os.rename(tmpname, world_filename) def load_world(): with open(world_filename, 'rb') as f: return pickle.load(f) async def repl(conn): world = conn.world conn.send(wrap(textwrap.dedent(""" Bienvenido al Lienzo de Multiples Usuarios. Podés crear un nuevo avatar escribiendo “nuevo” o podés poseer uno que ya existe escribiendo su verdadero nombre secreto. """))) while True: rawname = await conn.readline('nuevo, o verdadero nombre? ') name = rawname.lower().strip() if name == 'nuevo': player = new_avatar(world.contents[0]) conn.send(wrap(textwrap.dedent(""" Listo, tu verdadero nombre secreto es {}. Nunca lo digas a nadie, porque te pueden poseer después si lo saben. El mundo te conocerá por {} por ahora, pero podés eligir otro nombre cuando quieras. """.format(player.truename, player.name)))) world.avatars.append(player) save_world(world) break await truename_tries.take_token() avatars = [a for a in world.avatars if a.truename.lower() == name] if not avatars: conn.send("No hay ningún ser que se llama “{}” o “{}”.".format(rawname, name)) continue player = avatars[0] break conn.avatar = player connections[player] = conn player.wake_up() player.tell(player.container.describe()) while True: line = await conn.readline('? ') try: line = line.strip() if not line: continue if line.startswith('.'): verbs['hacer'](player, world, line[1:]) continue if line.startswith('-'): verbs['decir'](player, world, line[1:]) continue verb = line.split()[0] if verb in verbs: verbs[verb](player, world, line[len(verb):].strip()) else: verb = random.choice(list(verbs)) player.tell('No entiendo “{}”; tal vez probá algo como “{}”.'.format(line, verb)) except EOFError: break except Exception: traceback.print_exc() player.tell('Uh, algo está roto en el LMU, perdón.') else: save_world(world) # Save the world! class TokenBucket: "A token-bucket rate limit for asyncio tasks." def __init__(self, capacity=5, recharge=1, initial=None): self.capacity = capacity # maximum number of tokens self.recharge = recharge # number of seconds between recovering a token self.tokens = capacity if initial is None else initial self.last_update = time.time() async def take_token(self): self.update() while not self.tokens: await asyncio.sleep(self.recharge / 10) self.update() self.tokens -= 1 def update(self): now = time.time() while (self.tokens < self.capacity and now - self.last_update > self.recharge): self.tokens += 1 self.last_update += self.recharge truename_tries = TokenBucket() # XXX global mutable variable class Connection: def __init__(self, world, reader, writer): self.world = world self.reader = reader self.writer = writer self.avatar = None async def run(self): await repl(self) def send(self, s): return self.send_no_newline(s + '\n') def send_no_newline(self, s): if self.reader.at_eof(): self.kill() return self.writer.write(s.encode('utf-8', 'surrogateescape')) async def readline(self, prompt=None): if prompt is not None: self.send_no_newline(prompt) line = await self.reader.readline() if not line: raise EOFError(self.reader) return line.decode('utf-8', 'surrogateescape') def kill(self): self.writer.close() if self.avatar: if self.avatar in connections: del connections[self.avatar] self.avatar.put_to_sleep() self.avatar = None def handle(world): async def handle_connection(reader, writer): conn = Connection(world, reader, writer) try: await conn.run() finally: conn.kill() return handle_connection async def main(): try: world = load_world() except IOError: world = basic_world() for avatar in world.avatars: avatar.asleep = True server = await asyncio.start_server(handle(world), '', 7666) print("listening on", server.sockets[0].getsockname()) # In 3.5.2, asyncio.base_events.Server isn’t an async context # manager, and there’s no Server.serve_forever. await server.wait_closed() def asynciorun(main): "There’s no asyncio.run in Python 3.5." # # shows how to clean things up nicely. loop = asyncio.get_event_loop() loop.run_until_complete(main) loop.close() if __name__ == '__main__': asynciorun(main())