Привет. Меня зовут Николай Пискунов, я руководитель направления Big Data. В Beeline Cloud у нас есть место для экспериментов — и я этим пользуюсь. Недавно я работал над шахматным ботом для игры по переписке в Телеграм. Одна из ключевых задач — генерация изображений шахматной доски из FEN-нотации.
FEN (Forsyth-Edwards Notation) — текстовый формат записи шахматной позиции. Пример:
rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1
Пользователь хочет видеть красивое изображение, а не просто текст. Но готовых решений, которые удовлетворяли бы требованиям, не нашлось:
Производительность — генерация должна быть быстрой.
Кастомизация — нужны разные виды доски (для белых/черных).
Подсветка — выделение последнего хода.
Кеширование — чтобы не генерировать одно и то же изображение дважды.

Архитектура решения
Мое решение FenToImageConverter — сервис на Java, который:
принимает FEN-строку и параметры отображения;
генерирует картинку шахматной доски;
поддерживает кеширование;
возвращает изображение в формате PNG.
@Service public class FenToImageConverter { public BufferedImage convertFenToImage( String fen, boolean whiteView, String[] highlightedSquares ) { // Основная логика конвертации } }
Техническая реализация
1. Парсинг FEN
Первая задача — корректно распарсить FEN-строку:
public class FenValidator { // Регулярное выражение для валидации FEN private static final Pattern FEN_PATTERN = Pattern.compile( "^([rnbqkpRNBQKP1-8]+/){7}[rnbqkpRNBQKP1-8]+\s[wb]\s([KQkq]+|-)\s([a-h][36]|-)\s\d+\s\d+$" ); public boolean isValidFen(String fen) { // Проверка структуры FEN // Валидация количества фигур // Проверка корректности позиции } }
Нужно учесть, что FEN может быть некорректным. Цифры надо обрабатывать как пустые клетки и проверять наличие обоих королей.
2. Отрисовка доски
Самая интересная часть — рендеринг:
private static void drawBoard(Graphics2D g2d) { for (int row = 0; row < 8; row++) { for (int col = 0; col < 8; col++) { int x = BORDER_SIZE + col * SQUARE_SIZE; int y = BORDER_SIZE + row * SQUARE_SIZE; // Чередование цветов клеток boolean isLight = (row + col) % 2 == 0; g2d.setColor(isLight ? LIGHT_SQUARE : DARK_SQUARE); g2d.fillRect(x, y, SQUARE_SIZE, SQUARE_SIZE); } } }
Цвета клеток:
private static final Color LIGHT_SQUARE = new Color(240, 217, 181); // Светлая клеткаprivate static final Color DARK_SQUARE = new Color(181, 136, 99); // Темная клеткаprivate static final Color HIGHLIGHT_COLOR = new Color(255, 255, 0, 50); // Подсветка
3. Размещение фигур
Фигуры отображаются с помощью Unicode-символов:
private static String getPieceSymbol(char piece) { return switch (piece) { case 'K' -> "♔"; // Белый король case 'Q' -> "♕"; // Белый ферзь case 'R' -> "♖"; // Белая ладья case 'B' -> "♗"; // Белый слон case 'N' -> "♘"; // Белый конь case 'P' -> "♙"; // Белая пешка case 'k' -> "♚"; // Черный король case 'q' -> "♛"; // Черный ферзь case 'r' -> "♜"; // Черная ладья case 'b' -> "♝"; // Черный слон case 'n' -> "♞"; // Черный конь case 'p' -> "♟"; // Черная пешка default -> String.valueOf(piece); }; }
Есть проблема со шрифтами: не все они поддерживают Unicode-символы шахматных фигур. Вот как решить эту проблему:
private static Font getPieceFont() { String[] preferredFonts = { "Segoe UI Symbol", "Arial Unicode MS", "DejaVu Sans", "Arial" }; for (String fontName : preferredFonts) { Font font = new Font(fontName, Font.PLAIN, PIECE_FONT_SIZE); if (font.getFamily().equals(fontName)) { return font; } } return new Font(Font.SANS_SERIF, Font.PLAIN, PIECE_FONT_SIZE); }
4. Поддержка разных видов доски
Пользователь может смотреть доску за белых или за черных:
// Если whiteView = false, переворачиваем доску if (!whiteView) { // Переворачиваем вертикально List<String> revRows = new ArrayList<>(Arrays.asList(rows)); Collections.reverse(revRows); // Переворачиваем горизонтально revRows.replaceAll(FenToImageConverter::reverseRow); rows = revRows.toArray(new String[0]); } private static String reverseRow(String row) { // Преобразование строки FEN в обратном порядке // Пример: "r1bqkbnr" → "rnbkq1br" }
5. Подсветка клеток
Для наглядности подсвечиваем клетки последнего хода:
if (highlightedSquares != null && highlightedSquares.length > 0) { g2d.setColor(HIGHLIGHT_COLOR); for (String sq : highlightedSquares) { // Конвертация шахматной нотации в координаты int col = sq.charAt(0) - 'a'; int row = 8 - Character.getNumericValue(sq.charAt(1)); // Корректировка координат для черного вида if (!whiteView) { col = 7 - col; row = 7 - row; } int x = BORDER_SIZE + col * SQUARE_SIZE; int y = BORDER_SIZE + row * SQUARE_SIZE; g2d.fillRect(x, y, SQUARE_SIZE, SQUARE_SIZE); } }
6. Кеширование изображений
Чтобы не перерисовывать одно и то же изображение многократно, делаем кеширование:
@Configuration @EnableCaching public class CacheConfig { @Bean public CacheManager cacheManager() { return new ConcurrentMapCacheManager("boardImages"); } } @Service public class FenToImageConverter { @Cacheable( value = "boardImages", key = "#fen + #whiteView + (#highlightedSquares != null ? Arrays.toString(#highlightedSquares) : '')" ) public BufferedImage convertFenToImage( String fen, boolean whiteView, String[] highlightedSquares ) { // Генерация изображения } }
Ключ кеша включает все параметры, влияющие на результат:
FEN-строка,
вид доски (whiteView),
подсвеченные клетки.
Производительность
Я протестировал скорость генерации изображений и получил такие результаты:

Оптимизации:
Кеширование — основной прирост производительности.
Reusable Graphics2D — переиспользование объектов.
Font caching — кеширование шрифтов.
Color caching — кеширование объектов Color.
Результат
Пример сгенерированного изображения для FEN:
r1bqkbnr/pppp1ppp/2n5/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R w KQkq - 2 3

Тестирование
Для такого компонента критически важны тесты:
@Test public void testValidFenConversion() { String fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"; BufferedImage image = converter.convertFenToImage(fen, true, null); assertNotNull(image); assertEquals(TOTAL_SIZE, image.getWidth()); assertEquals(TOTAL_SIZE, image.getHeight()); } @Test public void testInvalidFenThrowsException() { String invalidFen = "invalid fen string"; assertThrows(IllegalArgumentException.class, () -> { converter.convertFenToImage(invalidFen, true, null); }); }
Проблемы и решения
Для удобства и наглядности я составил таблицу возможных проблем и способов с ними справиться.

Альтернативные подходы
SVG-генерация — сложнее, но масштабируется лучше.
HTML/CSS рендеринг — требует браузера.
Готовые библиотеки — недостаточная кастомизация.
WebGL/Canvas — избыточно для задачи.
В таблице — сравнение моего решения с альтернативами:

Заключение
Создание FEN-to-Image оказалось интересной задачей на стыке:
парсинга текстовых форматов,
компьютерной графики,
оптимизации производительности,
кеширования.
Получился высокопроизводительный компонент, который генерирует красивые изображения шахматных досок за 15–25 мс.
Полезные ссылки:
Beeline Cloud — безопасный облачный провайдер. Разрабатываем облачные решения, чтобы вы предоставляли клиентам лучшие сервисы.
