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

Misc
Задача «Скоростные Пазлы #2»
В первой части было всего 16 кусочков, которые я расставил руками), в этой уже 64, руками не получается, значит, попробуем написать скрипт. Смотрим исходный код страницы.
В script.js игра общается с сервером через два запроса. GET /api/new-game (fetch('/api/new-game') без параметра method — это GET) возвращает originalImage, массив puzzle из 64 кусочков, gridSize и pieceSize.
POST /api/check-solution принимает JSON {"solution": [...]}, где solution — ID кусочков в порядке клеток поля. При правильном порядке в ответе приходит flag.
Перетаскивать кусочки мышью не обязательно. Сервер сам отдаёт оригинальное изображение и все кусочки, причём каждый кусочек — точный фрагмент оригинала. Поэтому правильный порядок можно вычислить: режем оригинал на 8 × 8 плиток и для каждого кусочка ищем плитку с минимальной разницей пикселей, по которой наш скрипт сопоставляет куски и присваивает значение их порядка.
Важная деталь: состояние игры хранится в сессии express-session в cookie connect.sid. Если открыть new-game в одном запросе, а check-solution отправить без этой cookie, сервер ответит No active game session. Значит, оба запроса надо делать из одной сессии.
Порядок решения:
Скрипт на Python:
import base64
import io
import numpy as np
import requests
from PIL import Image
BASE = "https://fast-puzzles-2.t1.bug-makers.ru"
def decode(uri):
payload = uri.split(",", 1)[1]
return Image.open(io.BytesIO(base64.b64decode(payload))).convert("RGB")
session = requests.Session()
game = session.get(f"{BASE}/api/new-game", timeout=30).json()
n = game["gridSize"]
original = np.asarray(decode(game["originalImage"]), dtype=np.float32)
h, w, _ = original.shape
th, tw = h // n, w // n
tiles = [
original[r * th:(r + 1) * th, c * tw:(c + 1) * tw]
for r in range(n)
for c in range(n)
]
pieces = [
np.asarray(decode(p["image"]).resize((tw, th)), dtype=np.float32)
for p in game["puzzle"]
]
dist = np.array([
[np.mean((piece - tile) ** 2) for tile in tiles]
for piece in pieces
])
try:
from scipy.optimize import linear_sum_assignment
rows, cols = linear_sum_assignment(dist)
match = dict(zip(rows, cols))
except ImportError:
match = {}
used = set()
for _, i, j in sorted(
(dist[i, j], i, j)
for i in range(len(pieces))
for j in range(len(tiles))
):
if i not in match and j not in used:
match[i] = j
used.add(j)
solution = [None] * (n * n)
for i, piece in enumerate(game["puzzle"]):
solution[match[i]] = piece["id"]
response = session.post(
f"{BASE}/api/check-solution",
json={"solution": solution},
timeout=30,
).json()
print(response)
print("Флаг:", response["flag"])Что делает скрипт: сначала запускает новую игру и сохраняет значения cookie. После декодирует наше изображение из base64, сравнивает куски изображения по MSE, собирает куски по плиткам в порядке слева направо, сверху вниз и отправляет в check-solution.
Ставим зависимости, если у вас их нет (python -m pip install requests pillow numpy scipy), и запускаем скрипт, получая флаг.