Initial commit
This commit is contained in:
parent
5dc2fc5881
commit
d23df9c51c
|
@ -0,0 +1,129 @@
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from socketio import AsyncSimpleClient
|
||||||
|
from aiohttp import ClientSession
|
||||||
|
from PIL import Image, ImageFont, ImageDraw, ImageFilter
|
||||||
|
from base64 import b64decode
|
||||||
|
from random import choice
|
||||||
|
from json import load
|
||||||
|
|
||||||
|
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.difference: dict[int, bool] = {}
|
||||||
|
self._shutdown: bool = False
|
||||||
|
|
||||||
|
def put_text(self, x: int, y: int, text: str):
|
||||||
|
with Image.new("LA", (int(self.font.getlength(text) + 12), 16)) as im:
|
||||||
|
draw = ImageDraw.Draw(im)
|
||||||
|
draw.rectangle((0, 0, im.width, im.height), (0, 0))
|
||||||
|
draw.text((6, 5), text, font=self.font, fill=(255, 0))
|
||||||
|
|
||||||
|
alpha = im.convert("L").filter(ImageFilter.MaxFilter(3))
|
||||||
|
im.putalpha(alpha)
|
||||||
|
self.put_image(x, y, im)
|
||||||
|
|
||||||
|
def put_image(self, ox: int, oy: int, im: Image.Image):
|
||||||
|
for y in range(im.height):
|
||||||
|
for x in range(im.width):
|
||||||
|
l, a = im.getpixel((x, y)) # type: ignore
|
||||||
|
if a:
|
||||||
|
self.difference[x + ox + (y + oy) * 1000] = l > 0
|
||||||
|
|
||||||
|
|
||||||
|
async def listener(self):
|
||||||
|
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))
|
||||||
|
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()
|
||||||
|
elif event == "batched_bit_toggles":
|
||||||
|
bits_on, bits_off, timestamp = data
|
||||||
|
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)
|
||||||
|
|
||||||
|
async def writer(self):
|
||||||
|
async with ClientSession() as http:
|
||||||
|
async with AsyncSimpleClient(http_session=http) as sio:
|
||||||
|
await sio.connect(f"{self.base}/socket.io")
|
||||||
|
while not self._shutdown:
|
||||||
|
try:
|
||||||
|
event, data = await sio.receive(0.1)
|
||||||
|
if event not in ("full_state", "batched_bit_toggles"):
|
||||||
|
print("!!!", event, data)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
index, target = choice(list(self.difference.items()))
|
||||||
|
y, x = divmod(index, 1000)
|
||||||
|
curr = self.canvas.getpixel((x, y)) > 0 # type: ignore
|
||||||
|
if curr != target:
|
||||||
|
print("swap", x, y, index)
|
||||||
|
await sio.emit("toggle_bit", {
|
||||||
|
"index": index
|
||||||
|
})
|
||||||
|
await asyncio.sleep(0.25)
|
||||||
|
else:
|
||||||
|
await asyncio.sleep(0.001)
|
||||||
|
|
||||||
|
async def __aenter__(self):
|
||||||
|
self._listener_task = asyncio.create_task(self.listener())
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, a, b, c):
|
||||||
|
self._shutdown = True
|
||||||
|
await self._listener_task
|
||||||
|
|
||||||
|
|
||||||
|
async def amain():
|
||||||
|
with open("settings.json", "r") as fp:
|
||||||
|
settings = load(fp)
|
||||||
|
|
||||||
|
async with AsyncBotManager() as mgr:
|
||||||
|
mgr.font = ImageFont.truetype(settings["font"], 8)
|
||||||
|
for elem in settings["elements"]:
|
||||||
|
if elem["type"] == "text":
|
||||||
|
mgr.put_text(elem["text"], elem["x"], elem["y"])
|
||||||
|
elif elem["type"] == "image":
|
||||||
|
with Image.open(elem["path"]).convert("LA") as im:
|
||||||
|
mgr.put_image(elem["x"], elem["y"], im)
|
||||||
|
elif elem["type"] == "rgb111":
|
||||||
|
ox, oy = elem["x"], elem["y"]
|
||||||
|
with Image.open(elem["path"]).convert("RGB") as im:
|
||||||
|
for y in range(im.height):
|
||||||
|
for x in range(im.width):
|
||||||
|
r, g, b = im.getpixel((x, y)) # type: ignore
|
||||||
|
pocket_x = x + ox
|
||||||
|
pocket_y = y + oy
|
||||||
|
|
||||||
|
ndx_start = pocket_x * 3 + pocket_y * 577 * 3
|
||||||
|
mgr.difference[ndx_start] = r > 128
|
||||||
|
mgr.difference[ndx_start + 1] = g > 128
|
||||||
|
mgr.difference[ndx_start + 2] = b > 128
|
||||||
|
|
||||||
|
await asyncio.gather(*[
|
||||||
|
mgr.writer() for _ in range(settings["n_bots"])
|
||||||
|
], return_exceptions=True)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(amain())
|
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
Binary file not shown.
After Width: | Height: | Size: 43 KiB |
Binary file not shown.
After Width: | Height: | Size: 763 B |
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 1.0 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.6 KiB |
|
@ -0,0 +1,111 @@
|
||||||
|
from base64 import b64decode
|
||||||
|
import tkinter as tk
|
||||||
|
from PIL import Image, ImageTk
|
||||||
|
from socketio import SimpleClient, exceptions
|
||||||
|
from threading import Thread
|
||||||
|
from requests import get
|
||||||
|
|
||||||
|
COLORS = [(0x33, 0x33, 0x66), (0x96, 0x96, 0xe0)]
|
||||||
|
COLORS_UNPACKED = COLORS[0] + ((0, 0, 0) * 254) + COLORS[1]
|
||||||
|
|
||||||
|
|
||||||
|
class App(tk.Tk):
|
||||||
|
def __init__(self, url: str = "https://onemillioncheckboxes.com") -> None:
|
||||||
|
self.url = url
|
||||||
|
super().__init__()
|
||||||
|
self.title("1M pixels")
|
||||||
|
|
||||||
|
self.sio = SimpleClient()
|
||||||
|
|
||||||
|
self._canvas = tk.Canvas(self)
|
||||||
|
self._canvas.config(width=1000, height=1000, borderwidth=0, highlightthickness=0)
|
||||||
|
self._canvas.pack()
|
||||||
|
self.config(width=1000, height=1000, borderwidth=0, highlightthickness=0)
|
||||||
|
|
||||||
|
self._running = False
|
||||||
|
|
||||||
|
self._image = Image.new("RGB", (1000, 1000), COLORS[0])
|
||||||
|
|
||||||
|
self._canvas.bind("<Motion>", self._on_mouse_move)
|
||||||
|
self._canvas.bind("<Button>", self._on_mouse_click)
|
||||||
|
self.bind("<KeyPress>", self._on_key_down)
|
||||||
|
|
||||||
|
|
||||||
|
def _on_mouse_move(self, event: tk.Event):
|
||||||
|
self.title(f"1M pixels: {event.x}, {event.y}")
|
||||||
|
|
||||||
|
def _on_mouse_click(self, event: tk.Event):
|
||||||
|
x, y = event.x, event.y
|
||||||
|
self.sio.emit("toggle_bit", { "index": x + y * 1000 })
|
||||||
|
|
||||||
|
def _on_key_down(self, event: tk.Event):
|
||||||
|
# <KeyPress event state=Control keysym=r keycode=27 char='\x12' x=538 y=556>
|
||||||
|
if event.keysym == "r":
|
||||||
|
print("FULL REFRESH")
|
||||||
|
with get(f"{self.url}/api/initial-state") as req:
|
||||||
|
buffer = b64decode(req.json()["full_state"].encode() + b"=")
|
||||||
|
img = Image.frombytes("1", (1000, 1000), buffer).convert("P")
|
||||||
|
img.putpalette(COLORS_UNPACKED)
|
||||||
|
self._image.paste(img.convert("RGB"))
|
||||||
|
print("FULL REFRESH DONE")
|
||||||
|
|
||||||
|
def _reader(self):
|
||||||
|
while self._running:
|
||||||
|
try:
|
||||||
|
name, data = self.sio.receive()
|
||||||
|
except exceptions.TimeoutError:
|
||||||
|
print("Timeout")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if name == "batched_bit_toggles":
|
||||||
|
bits_on, bits_off, timestamp = data
|
||||||
|
for ndx in bits_on:
|
||||||
|
y, x = divmod(ndx, 1000)
|
||||||
|
self._image.putpixel((x, y), COLORS[1])
|
||||||
|
for ndx in bits_off:
|
||||||
|
y, x = divmod(ndx, 1000)
|
||||||
|
self._image.putpixel((x, y), COLORS[0])
|
||||||
|
self._tk_image.paste(self._image)
|
||||||
|
print("partial update", len(bits_on), len(bits_off))
|
||||||
|
|
||||||
|
elif name == "full_state":
|
||||||
|
print("full update")
|
||||||
|
buffer = b64decode(data["full_state"].encode() + b"=")
|
||||||
|
img = Image.frombytes("1", (1000, 1000), buffer).convert("P")
|
||||||
|
img.putpalette(COLORS_UNPACKED)
|
||||||
|
self._image.paste(img.convert("RGB"))
|
||||||
|
self._tk_image.paste(self._image)
|
||||||
|
|
||||||
|
else:
|
||||||
|
print(name, data)
|
||||||
|
|
||||||
|
self.sio.disconnect()
|
||||||
|
print("_reader exited")
|
||||||
|
|
||||||
|
def _close(self):
|
||||||
|
self._running = False
|
||||||
|
print("waiting for reader to close")
|
||||||
|
self._reader_thr.join()
|
||||||
|
self.destroy()
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
self.sio.connect(f"{self.url}/socket.io")
|
||||||
|
|
||||||
|
with get(f"{self.url}/api/initial-state") as req:
|
||||||
|
buffer = b64decode(req.json()["full_state"].encode() + b"=")
|
||||||
|
img = Image.frombytes("1", (1000, 1000), buffer).convert("P")
|
||||||
|
img.putpalette(COLORS_UNPACKED)
|
||||||
|
self._image.paste(img.convert("RGB"))
|
||||||
|
self._tk_image = ImageTk.PhotoImage(self._image)
|
||||||
|
self._tk_image_id = self._canvas.create_image(0, 0, anchor="nw", image=self._tk_image)
|
||||||
|
self._running = True
|
||||||
|
|
||||||
|
self._reader_thr = Thread(target=self._reader, name="Reader thread")
|
||||||
|
self._reader_thr.start()
|
||||||
|
|
||||||
|
self.protocol("WM_DELETE_WINDOW", self._close)
|
||||||
|
self.mainloop()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = App()
|
||||||
|
app.start()
|
|
@ -1,5 +0,0 @@
|
||||||
def main():
|
|
||||||
print('Hi!')
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
|
@ -1 +1,19 @@
|
||||||
|
aiohttp==3.9.5
|
||||||
|
aiosignal==1.3.1
|
||||||
|
attrs==23.2.0
|
||||||
|
bidict==0.23.1
|
||||||
|
certifi==2024.6.2
|
||||||
|
charset-normalizer==3.3.2
|
||||||
|
frozenlist==1.4.1
|
||||||
|
h11==0.14.0
|
||||||
|
idna==3.7
|
||||||
|
multidict==6.0.5
|
||||||
|
pillow==10.4.0
|
||||||
|
python-engineio==4.9.1
|
||||||
|
python-socketio==5.11.3
|
||||||
|
requests==2.32.3
|
||||||
|
simple-websocket==1.0.0
|
||||||
|
urllib3==2.2.2
|
||||||
|
websocket-client==1.8.0
|
||||||
|
wsproto==1.2.0
|
||||||
|
yarl==1.9.4
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
from base64 import b64decode
|
||||||
|
from PIL import Image
|
||||||
|
from requests import get
|
||||||
|
|
||||||
|
BASE_URL = "https://onemillioncheckboxes.com"
|
||||||
|
|
||||||
|
with get(f"{BASE_URL}/api/initial-state") as req:
|
||||||
|
data = req.json()
|
||||||
|
buffer = b64decode(data["full_state"].encode() + b"=")
|
||||||
|
|
||||||
|
def getbit(b: bytes, i: int) -> bool:
|
||||||
|
return b[i // 8] & (0x80 >> (i % 8)) != 0
|
||||||
|
|
||||||
|
with Image.new("RGB", (577, 577)) as im:
|
||||||
|
for y in range(im.height):
|
||||||
|
for x in range(im.width):
|
||||||
|
ndx_start = (x + y * 577) * 3
|
||||||
|
im.putpixel(
|
||||||
|
( x, y ),
|
||||||
|
(
|
||||||
|
255 if getbit(buffer, ndx_start) else 0,
|
||||||
|
255 if getbit(buffer, ndx_start + 1) else 0,
|
||||||
|
255 if getbit(buffer, ndx_start + 2) else 0,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
im.save("rgb111-full.png")
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
{
|
||||||
|
"url": "https://onemillioncheckboxes.com",
|
||||||
|
"font": "./ic8x8u.ttf",
|
||||||
|
"proxies": [
|
||||||
|
|
||||||
|
],
|
||||||
|
"n_bots": 10,
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "rgb111",
|
||||||
|
"path": "./casey111.png",
|
||||||
|
"x": 240,
|
||||||
|
"y": 240
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "rgb111",
|
||||||
|
"path": "./trans.png",
|
||||||
|
"x": 200,
|
||||||
|
"y": 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "image",
|
||||||
|
"path": "./casey.png",
|
||||||
|
"x": 256,
|
||||||
|
"y": 256
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "image",
|
||||||
|
"path": "./kangel.png",
|
||||||
|
"x": 256,
|
||||||
|
"y": 360
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "image",
|
||||||
|
"path": "./boykisser.png",
|
||||||
|
"x": 69,
|
||||||
|
"y": 420
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "image",
|
||||||
|
"path": "./lawbymike.png",
|
||||||
|
"x": 150,
|
||||||
|
"y": 420
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "image",
|
||||||
|
"path": "./colon3.png",
|
||||||
|
"x": 333,
|
||||||
|
"y": 333
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
Loading…
Reference in New Issue