Архитектура решения
Мое решение `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 мс.
Полезные ссылки: