Init. Commit
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
#!/usr/bin/env python
|
||||
"""Эталонный решатель задачи 1 «В глазах всё написано» (формат CTP1).
|
||||
|
||||
Независимая реализация: только stdlib + Pillow + numpy. Не импортирует код
|
||||
генератора и не читает файлы организатора.
|
||||
|
||||
Схема (восстанавливается из публичных улик в метаданных PNG):
|
||||
* сетка блоков 2x2 от левого верхнего угла, обход по строкам;
|
||||
* блок пригоден, если 4 цвета RGB попарно различны и размах каждого
|
||||
канала <= 3 (пригодность инвариантна к перестановке пикселей);
|
||||
* канонический порядок цветов: сортировка по ключу (77*R+150*G+29*B, R, G, B);
|
||||
* 24 перестановки (0,1,2,3) нумеруются лексикографически от 0,
|
||||
номера 0..15 кодируют полубайт, старший полубайт байта первым;
|
||||
* первые 16 пригодных блоков — калибровка (номера 0..15 по порядку);
|
||||
* далее конверт: magic "CTP1", version=1, payload_length u32 LE,
|
||||
payload, crc32 u32 LE от payload.
|
||||
|
||||
Использование: python solve_task1.py <participant.png>
|
||||
Печатает флаг (первая строка payload) в stdout. При ошибке — сообщение
|
||||
в stderr и ненулевой код возврата.
|
||||
"""
|
||||
|
||||
import struct
|
||||
import sys
|
||||
import zlib
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
RANGE_MAX = 3
|
||||
CALIBRATION_BLOCKS = 16
|
||||
MAGIC = b"CTP1"
|
||||
VERSION = 1
|
||||
MAX_PAYLOAD = 1 << 20 # 1 МиБ
|
||||
|
||||
# Все 24 перестановки (0,1,2,3) в лексикографическом порядке.
|
||||
LEX_PERMS = sorted(
|
||||
(a, b, c, d)
|
||||
for a in range(4)
|
||||
for b in range(4)
|
||||
for c in range(4)
|
||||
for d in range(4)
|
||||
if len({a, b, c, d}) == 4
|
||||
)
|
||||
PERM_INDEX = {p: i for i, p in enumerate(LEX_PERMS)}
|
||||
|
||||
|
||||
class SolveError(Exception):
|
||||
"""Ожидаемая ошибка решения: повреждённые или чужие данные."""
|
||||
|
||||
|
||||
def suitable(px):
|
||||
"""Пригодность блока 2x2. px — массив (4, 3) RGB в порядке TL,TR,BL,BR."""
|
||||
for i in range(4):
|
||||
for j in range(i + 1, 4):
|
||||
if px[i][0] == px[j][0] and px[i][1] == px[j][1] and px[i][2] == px[j][2]:
|
||||
return False
|
||||
lo = px.min(axis=0)
|
||||
hi = px.max(axis=0)
|
||||
return bool(((hi - lo) <= RANGE_MAX).all())
|
||||
|
||||
|
||||
def block_nibble(px):
|
||||
"""Номер лексикографической перестановки блока (0..23).
|
||||
|
||||
perm[k] = позиция k-го канонического цвета в порядке чтения блока.
|
||||
"""
|
||||
order = sorted(
|
||||
range(4), key=lambda i: (77 * px[i][0] + 150 * px[i][1] + 29 * px[i][2],
|
||||
px[i][0], px[i][1], px[i][2])
|
||||
)
|
||||
perm = [0] * 4
|
||||
for k, i in enumerate(order): # i — позиция в порядке чтения цвета C[k]
|
||||
perm[k] = i
|
||||
return PERM_INDEX[tuple(perm)]
|
||||
|
||||
|
||||
def solve(path):
|
||||
img = Image.open(path).convert("RGB")
|
||||
w, h = img.size
|
||||
arr = np.asarray(img, dtype=np.int32)
|
||||
|
||||
# Пригодные блоки 2x2 в порядке обхода по строкам; неполные краевые
|
||||
# блоки игнорируются.
|
||||
blocks = [] # (bx, by) левых верхних углов
|
||||
nibbles = []
|
||||
for by in range(0, h - 1, 2):
|
||||
for bx in range(0, w - 1, 2):
|
||||
px = arr[by:by + 2, bx:bx + 2].reshape(4, 3)
|
||||
if suitable(px):
|
||||
blocks.append((bx, by))
|
||||
nibbles.append(block_nibble(px))
|
||||
|
||||
if len(blocks) < CALIBRATION_BLOCKS:
|
||||
raise SolveError(
|
||||
f"слишком мало пригодных блоков: {len(blocks)}, нужно минимум 16"
|
||||
)
|
||||
|
||||
# Калибровка: первые 16 пригодных блоков обязаны давать 0..15 по порядку.
|
||||
for i in range(CALIBRATION_BLOCKS):
|
||||
if nibbles[i] != i:
|
||||
raise SolveError(
|
||||
f"калибровка нарушена: блок {i} даёт номер {nibbles[i]}, ожидался {i}"
|
||||
)
|
||||
|
||||
def read_byte(idx):
|
||||
"""Байт idx конверта: старший полубайт первым."""
|
||||
vals = []
|
||||
for half in range(2):
|
||||
pos = CALIBRATION_BLOCKS + 2 * idx + half
|
||||
if pos >= len(nibbles):
|
||||
raise SolveError("конверт усечён: не хватает пригодных блоков")
|
||||
n = nibbles[pos]
|
||||
if n > 15:
|
||||
raise SolveError(
|
||||
f"недопустимый номер перестановки {n} (допустимы 0..15) "
|
||||
f"в блоке {pos}"
|
||||
)
|
||||
vals.append(n)
|
||||
return (vals[0] << 4) | vals[1]
|
||||
|
||||
header = bytes(read_byte(i) for i in range(9))
|
||||
if header[:4] != MAGIC:
|
||||
raise SolveError(f"неверный magic: {header[:4]!r}, ожидался {MAGIC!r}")
|
||||
if header[4] != VERSION:
|
||||
raise SolveError(f"неподдерживаемая версия конверта: {header[4]}")
|
||||
(plen,) = struct.unpack("<I", header[5:9])
|
||||
if plen > MAX_PAYLOAD:
|
||||
raise SolveError(
|
||||
f"payload_length {plen} превышает лимит {MAX_PAYLOAD} байт"
|
||||
)
|
||||
total = 13 + plen
|
||||
if CALIBRATION_BLOCKS + 2 * total > len(blocks):
|
||||
raise SolveError(
|
||||
f"конверт усечён: нужно {total} байт, "
|
||||
f"блоков хватает на {(len(blocks) - CALIBRATION_BLOCKS) // 2}"
|
||||
)
|
||||
|
||||
rest = bytes(read_byte(i) for i in range(9, total))
|
||||
payload = rest[:plen]
|
||||
(crc_want,) = struct.unpack("<I", rest[plen:])
|
||||
crc_got = zlib.crc32(payload) & 0xFFFFFFFF
|
||||
if crc_got != crc_want:
|
||||
raise SolveError(
|
||||
f"несовпадение crc32: вычислено {crc_got:08x}, записано {crc_want:08x}"
|
||||
)
|
||||
return payload
|
||||
|
||||
|
||||
def main(argv):
|
||||
if len(argv) != 2:
|
||||
print(f"usage: {argv[0]} <participant.png>", file=sys.stderr)
|
||||
return 2
|
||||
try:
|
||||
payload = solve(argv[1])
|
||||
except SolveError as exc:
|
||||
print(f"error: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
except Exception as exc: # битый файл, не-PNG и т.п.
|
||||
print(f"error: не удалось обработать файл: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
try:
|
||||
text = payload.decode("utf-8")
|
||||
except UnicodeDecodeError as exc:
|
||||
print(f"error: payload не является UTF-8: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
flag = text.split("\n", 1)[0].strip()
|
||||
print(flag)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv))
|
||||
Reference in New Issue
Block a user