/Разборы задач
7 ноября 2026
Райтап на задачу «Двери»
Авторский разбор задачи «Двери»: принцип Port Knocking, поиск слушателя в подсети и последовательное открытие трёх TCP-портов.10
8

Misc
Задача «Скоростные Пазлы #3»
Во второй части было 64 кусочка, в этой уже 256.
В script.js игра общается с сервером через два запроса. GET /api/new-game (fetch('/api/new-game') без параметра method — это GET) возвращает originalImage, массив puzzle из 256 кусочков, gridSize и pieceSize.
POST /api/check-solution принимает JSON {"solution": [...]}, где solution — ID кусочков в порядке клеток поля. При правильном порядке в ответе приходит flag.
Сначала кажется, что подойдёт скрипт из второй части: разрезать оригинал на 16 × 16 плиток и сопоставить кусочки по MSE. Но не сработает. Причина в том, что кусочки не обязательно вырезаны строго с шагом pieceSize. Например, оригинал был 544 × 544, кусочки — 34 × 34, а реальные координаты кусочков идут с шагом около 33,5 пикселя. Если резать оригинал по 34 пикселя, часть кусочков сопоставляется неверно, и сервер отвечает success:false.
Нужно сначала найти каждый кусочек в оригинале, а уже потом определять его строку и столбец. Удобно использовать template matching: cv2.matchTemplate с TM_SQDIFF_NORMED — он возвращает координату кусочка в оригинале. По всем 256 координатам находим 16 центров по X и 16 центров по Y, после чего каждому кусочку ставим в соответствие ближайшую строку и столбец.
Скрипт на Python:
import base64
import io
import cv2
import numpy as np
import requests
from PIL import Image
BASE = "https://fast-puzzles-3.t1.bug-makers.ru"
def decode(uri):
payload = uri.split(",", 1)[1]
return Image.open(io.BytesIO(base64.b64decode(payload))).convert("RGB")
def centers(values, count):
groups = np.array_split(np.sort(values), count)
return np.array([np.median(group) for group in groups])
session = requests.Session()
game = session.get(f"{BASE}/api/new-game", timeout=60).json()
n = game["gridSize"]
original = cv2.cvtColor(
np.asarray(decode(game["originalImage"])),
cv2.COLOR_RGB2BGR,
)
locs = []
for piece in game["puzzle"]:
template = cv2.cvtColor(
np.asarray(decode(piece["image"])),
cv2.COLOR_RGB2BGR,
)
result = cv2.matchTemplate(original, template, cv2.TM_SQDIFF_NORMED)
_, _, loc, _ = cv2.minMaxLoc(result)
locs.append(loc)
xs = np.array([x for x, y in locs], dtype=float)
ys = np.array([y for x, y in locs], dtype=float)
cx = centers(xs, n)
cy = centers(ys, n)
rows = np.abs(ys[:, None] - cy[None, :]).argmin(axis=1)
cols = np.abs(xs[:, None] - cx[None, :]).argmin(axis=1)
cells = rows * n + cols
if len(set(cells.tolist())) != n * n:
from scipy.optimize import linear_sum_assignment
cost = np.array([
((x - cx[None, :]) ** 2 + (y - cy[:, None]) ** 2).reshape(-1)
for x, y in locs
])
row_idx, cell_idx = linear_sum_assignment(cost)
cells = np.full(len(locs), -1, dtype=int)
for i, cell in zip(row_idx, cell_idx):
cells[i] = cell
solution = [None] * (n * n)
for i, piece in enumerate(game["puzzle"]):
solution[int(cells[i])] = piece["id"]
response = session.post(
f"{BASE}/api/check-solution",
json={"solution": solution},
timeout=60,
).json()
print(response)
print("Флаг:", response["flag"])Скрипт делает то же самое, что и из части 2, но дополнительно он декодирует, через template matching находит координаты каждого кусочка, определяет его строку и столбец по 16 центрам X и Y.
Запускаем и получаем флаг.