This commit is contained in:
Casey 2024-07-05 23:13:39 +03:00
parent 42e88fce44
commit 26e8b804ee
Signed by: hkc
GPG Key ID: F0F6CFE11CDB0960
2 changed files with 100 additions and 76 deletions

View File

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

View File

@ -55,6 +55,12 @@
} }
], ],
"elements": [ "elements": [
{
"type": "rgb111",
"path": "./pictures/trans.png",
"x": 200,
"y": 200
},
{ {
"type": "image", "type": "image",
"path": "./pictures/kangel.png", "path": "./pictures/kangel.png",
@ -91,13 +97,6 @@
"path": "./pictures/niko.png", "path": "./pictures/niko.png",
"x": 48, "x": 48,
"y": 12 "y": 12
},
{
"type": "animation",
"path": "./pictures/loading.gif",
"x": 436,
"y": 436,
"spf": 20
} }
] ]
} }