Райтап на задачу «Скоростные Пазлы #3»

musa2ap

Автор:

musa2ap

7 ноября 2026

8

07.11.2026
Скоростные Пазлы #3
Категория:

Misc

Задача «Скоростные Пазлы #3»

LastTeamLead

Автор:

LastTeamLead

Первая кровь:
vanderson
Сложность:
medium
Дата публикации:
21.09.2025
Решений:
78
Рейтинг:
4.64/5
Ссылка:
Решать

Во второй части было 64 кусочка, в этой уже 256.

В script.js игра общается с сервером через два запроса. GET /api/new-game (fetch('/api/new-game') без параметра method — это GET) возвращает originalImage, массив puzzle из 256 кусочков, gridSize и pieceSize.

Скоростные Пазлы #3: Получение новой игры с 256 фрагментами в script.js

POST /api/check-solution принимает JSON {"solution": [...]}, где solution — ID кусочков в порядке клеток поля. При правильном порядке в ответе приходит flag.

Скоростные Пазлы #3: Проверка решения пазла и обработка ответа сервера

Сначала кажется, что подойдёт скрипт из второй части: разрезать оригинал на 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.

Запускаем и получаем флаг.

Если вы нашли опечатку или ошибку, пожалуйста, выделите фрагмент текста и нажмите Ctrl+Enter

Также по теме:

Райтап на задачу «Двери»

/Разборы задач

Райтап на задачу «Двери»

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

10

Райтап на задачу «Скоростные Пазлы #2»

/Разборы задач

Райтап на задачу «Скоростные Пазлы #2»

Автоматически собираем пазл из 64 фрагментов: изучаем API игры, сравниваем плитки по MSE и отправляем решение в одной сессии.
Прочитать

10