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])
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())

View File

@ -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
}
]
}