diff --git a/async-bot.py b/async-bot.py index d49e959..8d1798a 100644 --- a/async-bot.py +++ b/async-bot.py @@ -12,14 +12,16 @@ from time import time as time_now PixelMap = NewType("PixelMap", dict[int, bool]) Animation = NewType("Animation", tuple[list[PixelMap], float]) - +Font = ImageFont.FreeTypeFont | ImageFont.ImageFont class AsyncBotManager: def __init__(self, base: str = "https://onemillioncheckboxes.com"): self.base = base self.canvas = Image.new("1", (1000, 1000)) - self.font = ImageFont.load_default(8) + self.fonts: dict[tuple[str, int], Font] = { + ("default", 8): ImageFont.load_default(8) + } self.difference: PixelMap = PixelMap({}) self._last_update = 0 @@ -27,22 +29,34 @@ class AsyncBotManager: self.avoid: set[int] = set() self.animations: list[Animation] = [] - self._change_count = 0 - self._last_change_printout = time_now() + self._written_boxes = 0 + self._read_boxes = 0 + self._last_printout = time_now() @staticmethod def get_text_image(text: str, font: ImageFont.ImageFont | ImageFont.FreeTypeFont) -> Image.Image: - with Image.new("LA", (int(font.getlength(text) + 12), 16)) as im: + left, top, right, bottom = font.getbbox(text) + with Image.new("LA", (int(right - left) + 4, int(bottom - top) + 4), 0) as im: draw = ImageDraw.Draw(im) draw.rectangle((0, 0, im.width, im.height), (0, 0)) - draw.text((6, 5), text, font=font, fill=(255, 0)) + draw.text((left + 2, top + 2), text, font=font, fill=(255, 0)) - alpha = im.convert("L").filter(ImageFilter.MaxFilter(3)) + alpha = im.convert("L").filter(ImageFilter.MaxFilter(5)) im.putalpha(alpha) return im.copy() - def put_text(self, x: int, y: int, text: str): - self.put_image(x, y, self.get_text_image(text, self.font)) + def get_font(self, font_name: str, size: int = 8): + if (font := self.fonts.get((font_name, size))): + return font + if font_name == "default": + font = ImageFont.load_default(size) + else: + font = ImageFont.truetype(font_name, size) + self.fonts[font_name, size] = font + return font + + def put_text(self, x: int, y: int, text: str, font: str, size: int = 8): + self.put_image(x, y, self.get_text_image(text, self.get_font(font, size))) def put_image(self, ox: int, oy: int, im: Image.Image): for y in range(im.height): @@ -108,46 +122,55 @@ class AsyncBotManager: return im.copy() async def listener(self) -> None: - async with ClientSession() as http: - async with http.get(f"{self.base}/api/initial-state") as req: - data = await req.json() - buffer = b64decode(data["full_state"].encode() + b"=") - self.canvas.paste(Image.frombytes("1", (1000, 1000), buffer)) - self._last_update = data["timestamp"] - async with AsyncSimpleClient(http_session=http) as sio: - await sio.connect(f"{self.base}/socket.io") - while not self._shutdown: - event, data = await sio.receive() - if event == "full_state": - buffer = b64decode(data["full_state"].encode() + b"=") - image = Image.frombytes("1", (1000, 1000), buffer) - self.canvas.paste(image) - image.close() - self._last_update = data["timestamp"] - elif event == "batched_bit_toggles": - bits_on, bits_off, timestamp = data - if timestamp < self._last_update: - print("SKIPPING UPDATES: TOO OLD") - self._last_update = timestamp - for ndx in bits_on: - y, x = divmod(ndx, 1000) - self.canvas.putpixel((x, y), 255) - for ndx in bits_off: - y, x = divmod(ndx, 1000) - self.canvas.putpixel((x, y), 0) - else: - print("unknown event", event, data) - now = time_now() - if (now - self._last_change_printout) > 1: - cps = self._change_count / (now - self._last_change_printout) - print(f"Speed: {cps:.2f}/s") - self._change_count = 0 - self._last_change_printout = now - for pixmaps, spf in self.animations: - frame_index = int(now / spf) - self.difference.update( - pixmaps[frame_index % len(pixmaps)] - ) + try: + async with ClientSession() as http: + async with http.get(f"{self.base}/api/initial-state") as req: + data = await req.json() + buffer = b64decode(data["full_state"].encode() + b"=") + self.canvas.paste(Image.frombytes("1", (1000, 1000), buffer)) + self._last_update = data["timestamp"] + async with AsyncSimpleClient(http_session=http) as sio: + await sio.connect(f"{self.base}/socket.io") + while not self._shutdown: + event, data = await sio.receive() + if event == "full_state": + buffer = b64decode(data["full_state"].encode() + b"=") + image = Image.frombytes("1", (1000, 1000), buffer) + self.canvas.paste(image) + image.close() + self._last_update = data["timestamp"] + elif event == "batched_bit_toggles": + bits_on, bits_off, timestamp = data + if timestamp < self._last_update: + print("SKIPPING UPDATES: TOO OLD") + else: + self._last_update = timestamp + self._read_boxes += len(bits_on) + len(bits_off) + for ndx in bits_on: + y, x = divmod(ndx, 1000) + self.canvas.putpixel((x, y), 255) + for ndx in bits_off: + y, x = divmod(ndx, 1000) + self.canvas.putpixel((x, y), 0) + else: + print("unknown event", event, data) + now = time_now() + if (now - self._last_printout) > 1: + outgoing = self._written_boxes / (now - self._last_printout) + incoming = self._read_boxes / (now - self._last_printout) + print(f"Incoming: {incoming:7.2f}/s Outgoing: {outgoing:7.2f}/s") + self._written_boxes = 0 + self._read_boxes = 0 + self._last_printout = now + for pixmaps, spf in self.animations: + frame_index = int(now / spf) + self.difference.update( + pixmaps[frame_index % len(pixmaps)] + ) + except Exception as e: + print(f"Listener died: {e!r}") + self._shutdown = True + raise async def writer( self, @@ -157,21 +180,21 @@ class AsyncBotManager: ): proxy = ProxyConnector.from_url(proxy_url) if proxy_url else None async with ClientSession(connector=proxy) as http: - sio = AsyncClient(http_session=http) - await sio.connect(f"{self.base}/socket.io") - while not self._shutdown: - diff = list(self.difference.items()) - for _ in range(100): - index, expected = choice(diff) - y, x = divmod(index, 1000) - current = self.canvas.getpixel((x, y)) > 0 # type: ignore - if current != expected: - # print(f"[{bot_index:2d}] swap {x:3d} {y:3d}") - self._change_count += 1 - await sio.emit("toggle_bit", {"index": index}) - await asyncio.sleep(delay) - break - await asyncio.sleep(0.1) + async with AsyncSimpleClient(http_session=http) as sio: + await sio.connect(f"{self.base}/socket.io") + while not self._shutdown: + diff = list(self.difference.items()) + for _ in range(100): + index, expected = choice(diff) + y, x = divmod(index, 1000) + current = self.canvas.getpixel((x, y)) > 0 # type: ignore + if current != expected: + # print(f"[{bot_index:2d}] swap {x:3d} {y:3d}") + self._written_boxes += 1 + await sio.emit("toggle_bit", {"index": index}) + await asyncio.sleep(delay) + break + await asyncio.sleep(0.1) async def __aenter__(self): self._listener_task = asyncio.create_task(self.listener()) @@ -187,7 +210,6 @@ async def amain() -> None: settings = load(fp) async with AsyncBotManager() as mgr: - mgr.font = ImageFont.truetype(settings["font"], 8) for avoid in settings["avoid"]: if avoid["type"] == "rect": mgr.add_avoid_rect( @@ -207,11 +229,11 @@ async def amain() -> None: mgr.add_avoid_index(x + y * 1000) for elem in settings["elements"]: if elem["type"] == "text": - mgr.put_text(elem["x"], elem["y"], elem["text"]) + mgr.put_text(elem["x"], elem["y"], elem["text"], elem.get("font", "default"), elem.get("size", 8)) elif elem["type"] == "text_anim": frames: list[Image.Image] = [] for text in elem["lines"]: - frames.append(mgr.get_text_image(text, mgr.font)) + frames.append(mgr.get_text_image(text, mgr.get_font(elem.get("font", "default"), elem.get("size", 8)))) mgr.add_animation(elem["x"], elem["y"], frames, elem["spf"]) for frame in frames: frame.close() @@ -285,7 +307,7 @@ async def amain() -> None: print("Starting writers...") if n_proxies := len(settings["proxies"]): - await asyncio.gather( + res = await asyncio.gather( *[ mgr.writer( i, @@ -297,11 +319,14 @@ async def amain() -> None: return_exceptions=True, ) else: - await asyncio.gather( + res = await asyncio.gather( *[mgr.writer(i) for i in range(settings["n_bots"])], return_exceptions=True, ) + for ret in res: + print(ret) + if __name__ == "__main__": asyncio.run(amain()) diff --git a/settings.json b/settings.json index 797f610..79b905e 100644 --- a/settings.json +++ b/settings.json @@ -55,6 +55,12 @@ } ], "elements": [ + { + "type": "rgb111", + "path": "./pictures/trans.png", + "x": 200, + "y": 200 + }, { "type": "image", "path": "./pictures/kangel.png", @@ -91,13 +97,6 @@ "path": "./pictures/niko.png", "x": 48, "y": 12 - }, - { - "type": "animation", - "path": "./pictures/loading.gif", - "x": 436, - "y": 436, - "spf": 20 } ] }