STM32 Интерактивная шахматная доска.

alexlaw

✩✩✩✩✩✩✩
3 Янв 2020
113
2
Воронеж
Сформулируйте задачу для ИИ для особых ходов - рокировка, взятие на проходе, превращение только в ферзя.
Суть - автоматически делать нельзя. Т.к. на реальной физической доске это должен сделать пользователь.
Получается как бы два хода (напр. для рокировки) - переставить короля, переставить ладью.
Вот последняя версия touchChess.
Завтра приду с работы задам ИИ задачу))
 

Вложения

alexlaw

✩✩✩✩✩✩✩
3 Янв 2020
113
2
Воронеж
Пообщался с ИИ
Код:
Рассмотрим особые ходы.
Я назвал особыми ходы, которые состоят по сути из двух ходов.
1. Рокировки - нужно переместить короля на две клетки, затем ладью на соответствующее место.
ничего не проверяем, ни наличие короля на начальном поле,ни ладьи
2. Взятие на проходе - сначала пешкой стираем чужую пешку затем ставим на тто место, куда пешка в итоге должна встать.
из проверки только, что наша пешка находится на 6 горизонтали
3. Повышение в ранге - только в ферзя. (тут только один ход)
из проверок - только достижение пешкой 8 горизонтали

На время особыых ходов блокируем другие ходы пользователя, а именно
1.Для рокировки - должны поставить (скопировть) соответвующее поле для ладьи на соответствующее новое поле,
Если поле пустое - нелегальный ход сразу.
2.Длявзятие на проходе - должны сначала свою пешку поставить на чужую пешку(которую сЪедим),\
затем овое место (куда пешка в итоге должна встать)
3.Для Повышения в ранге - делаем автоматически

Только после этого передаем ход на сервер
1.Для рокировки - ход короля на две клетки
2.Для взятия на проходе - ход пешки откуда-куда
3.Для Повышения в ранге - ход пешки откуда-куда
Код:
Задача масштабная, но логика ясная. Добавляю три особых хода с детекцией и обработкой, плюс метод isCellEmpty для проверок.
Код:
Что добавлено
Метод / логика    Назначение
isCellEmpty(cellX, cellY)    Сравнивает клетку на bitmap3 с чистой доской bitmap1. Если совпадает — клетка пуста
movePiece(fromX, fromY, toX, toY)    Универсальный метод: extract → clear → clear → draw. Используется в applyMove для движка
Детекция рокировки    Math.abs(toFile - fromFile) == 2 && fromRank == toRank — горизонтальный ход на 2 клетки
Детекция взятия на проходе    Диагональный ход с 5-й горизонтали (rank 5, 0-indexed: 4) в пустую клетку
Детекция превращения    Целевая клетка на 8-й горизонтали (rank 8, 0-indexed: 7)
Особые ходы пользователя (белые)
Рокировка: двигаем короля → проверяем ладью (isCellEmpty) → если пусто, откат через bitmapBackup и Toast → если есть ладья, копируем её на новое место → отправляем только ход короля (например e1g1).

Взятие на проходе: очищаем нашу клетку → стираем чужую пешку на (XPol, selectedY) → ставим нашу пешку на целевую клетку → отправляем ход пешки (например e5d6).

Превращение: очищаем клетки → рисуем белого ферзя arrayOfBitmap[1] → отправляем ход пешки (например e7e8).

Особые ходы движка (чёрные)
В applyMove те же три типа детектируются автоматически:

Рокировка: fromRank == toRank и Math.abs(toFile - fromFile) == 2 → двигает короля и ладью
Взятие на проходе: fromRank == 3 (4-я горизонталь для чёрных), диагональ в пустую клетку → стирает белую пешку на (toCell[0], fromCell[1])
Превращение: toRank == 0 (1-я горизонталь) → рисует чёрного ферзя arrayOfBitmap[7]
Про горизонталь для взятия на проходе
Я использовал стандартные шахматные правила: белые — с 5-й горизонтали (fromRank == 4), чёрные — с 4-й (fromRank == 3). Ты говорил про 6-ю — если имелась в виду другая нумерация, поменяй константу 4 на нужную в строке isEnPassant.
Код:
Уточняю - для черных делаем автоматически, для белых в ручную должен сделать условных два хода пользователь (я указал в задаче, как именно)
Код:
Понял! Особые ходы для белых — это два ручных тапа (выбор + перемещение) каждый, а не автоматика. Код ждёт второй ход и только потом отправляет на сервер. Превращение — автоматически. Для чёрных (движка) — всё автоматически как было.

Добавил поля pendingSpecial, pendingMove, pendingFromNum и новую ветку логики в onTouchEvent. Вот полный код:

Как работают особые ходы белых
Рокировка (4 тапа):

[LIST=1]
[*]Тап на короля → тап на клетку в 2 клетки от него (e1 → g1).
[*]Код: двигает короля, проверяет ладью. Если ладьи нет — откат + Toast. Если есть — Toast «Сделайте ход ладьёй», pendingSpecial = CASTLING.
[*]Тап на ладью → тап на новое место (h1 → f1).
[*]Код: двигает ладью, отправляет ход короля e1g1 на сервер.
[/LIST]
Взятие на проходе (4 тапа):

[LIST=1]
[*]Тап на свою пешку → тап на чужую пешку (соседняя вертикаль, та же горизонталь, e5 → d5).
[*]Код: стирает чужую, ставит нашу на её место. Toast «Сделайте ход пешкой», pendingSpecial = EN_PASSANT, запоминает исходную клетку.
[*]Тап на свою пешку → тап на целевую клетку (d5 → d6).
[*]Код: двигает пешку, формирует ход e5d6 (исходная → целевая) и отправляет на сервер.
[/LIST]
Превращение (2 тапа, автоматически):

[LIST=1]
[*]Тап на пешку → тап на 8-ю горизонталь.
[*]Код: рисует ферзя, отправляет ход на сервер.
[/LIST]
Про горизонталь взятия на проходе
Я использовал fromRank == 4 (5-я горизонталь, 0-indexed) — стандартные шахматные правила. Ты говорил про «6-ю горизонталь». Если ты считаешь с 1 и твоя 6-я = rank 5 (0-indexed), поменяй fromRank == 4 на fromRank == 5 в строке детекции isEnPassant.
Я:
Ты говорил про «6-ю горизонталь» Я ошибся
Код:
Всё в порядке — код уже использует правильное значение fromRank == 4 (5-я горизонталь в шахматной нумерации), так что менять ничего не нужно.
Можешь тестировать особые ходы. Если что-то отрисуется не так — пиши, поправим.
Код:
Буду проверять
Код:
Отлично! Тестируй спокойно — если заметишь, что:

* фигура «залипает» или рисуется не там;
* особый ход не запускается/не завершается;
* сервер не получает ход или возвращает ошибку;
* откат позиции (bitmapBackup) не срабатывает,

— просто скажи, какой ход делал и что увидел (можно даже скрин логов из TextView). Я быстро подкручу логику.

Удачи с проверкой! Если всё ок — напишу пару идей, как сделать обработку ошибок сервера чуть надёжнее. 😊
Код:
Сразу обнаружил существенный косяк.
Слон на 8 горизонтали превратился в ферзя.
Давай подумаем, что можно сделать
Код:
Проблема ясна: проверка toRank == 7 срабатывает на любую фигуру, дошедшую до 8-й горизонтали, а не только на пешку. Нужно определять, что в bitmap5 именно пешка.

После extractFigure в bitmap5 лежит вырезанная фигура на прозрачном фоне. Сравним её с эталоном белой пешки arrayOfBitmap[5] — с допуском 80%, чтобы антиалиасинг не мешал.

Добавь этот метод (например, рядом с bitmap5HasContent):

Теперь превращение сработает только если в bitmap5 действительно пешка, а не слон. Аналогично взятие на проходе — только для пешки.

Для чёрных (в applyMove) можно оставить как есть: движок присылает только легальные ходы, так что если он шлёт ход на 1-ю горизонталь — это гарантированно превращение пешки.
Код:
Хорошо,дай полный код со своими улучшениями и что ты говорил про — напишу пару идей, как сделать обработку ошибок сервера чуть надёжнее.
Код:
Что добавлено по сравнению с прошлой версией
Добавление    Зачем
isWhitePawnInBitmap5()    Сравнивает bitmap5 с эталоном белой пешки (допуск 80%). Превращение и взятие на проходе срабатывают только для пешки
startServerTimeout() / stopServerTimeout()    Запускает таймер на 10 секунд при отправке хода. Если сервер не ответил — откат позиции, Toast, возврат в IDLE
Обрыв соединения в startReaderThread    Если поток чтения вылетает (socket закрылся), восстанавливаем позицию из бэкапа и показываем Toast «Соединение потеряно»
stopServerTimeout() в onBackPressed / onDestroy    Таймер не сработает после выхода
Идеи по дальнейшему улучшению обработки ошибок
1. Буферизация неполных строк. Сейчас сервер может прислать mov e2e одним пакетом и 4 w@ следующим. Код не найдёт w@ в первом и зависнет. Решение: накапливать текст в отдельной переменной lineBuffer, искать полные строки (заканчивающиеся на \n), и только их анализировать.

2. Различение w@ и b@ по контексту. Сейчас если сервер случайно пришлёт mov ... b@ во время WAITING_CONFIRMATION, код его проигнорирует. Это правильно, но можно добавить Toast «Неожиданный ответ» для отладки.

3. Авто-восстановление позиции. При разрыве соединения сейчас откатывается только последний ход. Если пользователь сделал несколько ходов без ответа (маловероятно, но возможно при рассинхроне), хорошо бы иметь полный снимок доски из FEN и перерисовать.

4. Отдельный поток для отправки. Сейчас SendTask — AsyncTask, но при бырых последовательных отправках (ход + 0) задачи могут встать в очередь. Можно использовать единый поток отправки или synchronized на mOutputBuffer.

Какие из этих идей хочешь внедрить?
Код:
Давай все эти идеи внедрим
Код:
Объединил все четыре улучшения в один код. Основные изменения:

Буферизация строк — lineBuffer накапливает текст, анализ идёт только по завершённым строкам (с \n)
Контекстный разбор — если сервер шлёт b@ во время WAITING_CONFIRMATION или w@ во время WAITING_ENGINE, показываем Toast «Неожиданный ответ»
Авто-восстановление — при разрыве соединения перерисовываем доску из bitmapBackup + сброс pendingSpecial
Единый поток отправки — SendThread вместо AsyncTask, с synchronized на mOutputBuffer

Что изменилось — сводка
1. Буферизация строк (lineBuffer + processIncomingData)

Раньше: сервер присылал mov e2e и 4 w@ двумя пакетами — код не находил w@ в первом и зависал.

Теперь: processIncomingData накапливает текст в lineBuffer, вытаскивает завершённые строки (до \n) и передаёт каждую в analyzeServerLine. Неполный остаток ждёт следующего пакета.

2. Контекстный разбор (analyzeServerLine)

Раньше: checkServerResponse искал lastIndexOf("mov ") во всём накопленном тексте.

Теперь: каждая строка анализируется отдельно. Если во время WAITING_CONFIRMATION приходит b@ — Toast «Неожиданный ответ сервера (b@)». Если во время WAITING_ENGINE приходит w@ — аналогично. Плюс # обрабатывается в обоих состояниях.

3. Авто-восстановление (restorePosition)

Раньше: при разрыве соединения откатывался только bitmap3, но pendingSpecial, hasSelection и selectedX/Y оставались в подвешенном состоянии.

Теперь: restorePosition() сбрасывает всё — bitmap3 из бэкапа, bitmap5 прозрачный, hasSelection = false, pendingSpecial = SPECIAL_NONE. Вызывается из:

таймаута
обрыва соединения
отклонения хода (#)
onBackPressed
4. Единый поток отправки (SendThread + sendToServer)

Раньше: SendTask (AsyncTask) — при быстрых последовательных отправках (ход + 0) задачи могли встать в очередь или наложиться.

Теперь: sendToServer(cmd) кладёт команду в mSendQueue и будит mSenderThread. Поток извлекает команды по очереди и пишет в mOutputBuffer под synchronized. Гарантируется порядок отправки и отсутствие наложений. mButtonSend тоже использует sendToServer напрямую.
Код:
На сегодня хватит, откатимся к версии после слов - Полный код с проверкой пешки: А то в последней много косяков, точнее моментов требющих проверки
Код:
Понял, откатываемся к версии с проверкой пешки — без буферизации строк, без единого потока отправки и без авто-восстановления. Вот тот самый код:
 
Изменено:

alexlaw

✩✩✩✩✩✩✩
3 Янв 2020
113
2
Воронеж
Неплохо))
Теперь
Код:
неплохо распознает, теперь подвигаем фигурами как и раньше, хочу посмотреть как будет работать распознавание после перемещений, никаких серверов и ограничений на ходы - просто канвасы
Код:
Отлично, давай сделаем так: убираем сервер, убираем проверки ходов — оставляем только канвасы, тапы и распознавание. А главное — добавляем возможность брать фигуру и ставить её на другую клетку, чтобы ты мог вручную двигать фигуры и проверять, как identifyPiece() отрабатывает после перемещений.

Что добавлено
Перетаскивание: тап по фигуре — она «поднимается» (рисуется отдельно в bitmap5), второй тап — ставится на новую клетку.
Позиция хранится в int[8][8]: 0 — пусто, 1–12 — фигуры. Это нужно, чтобы корректно стирать старую фигуру и рисовать новую.
Без ограничений на ходы: куда ткнул — туда и ставится.
Распознавание работает как раньше: по вырезанному фрагменту доски.
Никаких лямбд и AndroidX: чистый старый Java для надёжной сборки в Java N‑IDE.
Код:
Как это работает для твоей задачи
Тап по фигуре — она исчезает с доски (визуально), появляется «в руке» (в углу, потому что рисуется bitmap5 в 0,0).
Второй тап — фигура ставится на новую клетку, доска перерисовывается, запускается extractFigure + identifyPiece, и ты получаешь окно с результатом распознавания.
Можно двигать сколько угодно, проверять, не «плывёт» ли распознавание после перемещений, смещений, наложений теней и т.п.
Никаких серверов, никаких проверок ходов — только канвасы и логика перетаскивания.
Важные моменты под твой стек
Java N‑IDE: нет лямбд, нет AndroidX — должно собраться без ошибок.
Память: bitmap5 переиспользуется, board[][] хранит состояние — это самый простой и надёжный вариант для отладки.
Проверка распознавания: после каждого хода ты сразу видишь окно с результатом — удобно для тестов.
Если хочешь, могу добавить:

Рисование фигуры «в руке» прямо под пальцем (вместо 0,0), чтобы было похоже на Drag&Drop.
Вывод результата не в окне, а в TextView на экране, чтобы не закрывать окно каждый раз.
Логирование в Logcat для анализа порогов и баллов сравнения.
Что сейчас важнее: чтобы было максимально похоже на реальное перетаскивание или чтобы было проще для быстрой проверки распознавания?
 

Вложения

alexlaw

✩✩✩✩✩✩✩
3 Янв 2020
113
2
Воронеж
Пришли светодиоды)))
Надо переключаться на другие задачи.
Может кто продолжит Андроид проект?
Доведет до ума?
Надеюсь понятны идея и смысл чего я хотел добиться?
 

alexlaw

✩✩✩✩✩✩✩
3 Янв 2020
113
2
Воронеж
Буду заниматься светодиодами и герконами.
Выкладываю рабочий код сервера на ESP и клиента на Андроид.
C:
#include <ESP8266WiFi.h>
// config: ////////////////////////////////////////////////////////////
bool debug = false;
#define VERSION "1.10"
// For AP mode:
const char *ssid = "Serial_wifi";  // You will connect your phone to this Access Point
const char *pw = "123456789"; // and this is the password
IPAddress ip(192, 168, 4, 1); // From RoboRemo app, connect to this IP
IPAddress netmask(255, 255, 255, 0);
const char CharMap[96] = {
  ' ' , '!' , '"' , '#' , '$' , '%' , '&' , '\'' , '(' , ')' ,
  '*' , '+' , ',' , '-' , '.' , '/' , '0' , '1' , '2' , '3' ,
  '4' , '5' , '6' , '7' , '8' , '9' , ':' , ';' , '<' , '=' ,
  '>' , '?' , '@' , 'A' , 'B' , 'C' , 'D' , 'E' , 'F' , 'G' ,
  'H' , 'I' , 'J' , 'K' , 'L' , 'M' , 'N' , 'O' , 'P' , 'Q' ,
  'R' , 'S' , 'T' , 'U' , 'V' , 'W' , 'X' , 'Y' , 'Z' , '[' ,
  '\\' , ']' , '^' , '_' , '`' , 'a' , 'b' , 'c' , 'd' , 'e' ,
  'f' , 'g' , 'h' , 'i' , 'j' , 'k' , 'l' , 'm' , 'n' , 'o' ,
  'p' , 'q' , 'r' , 's' , 't' , 'u' , 'v' , 'w' , 'x' , 'y' ,
  'z' , '{' , '|' , '}' , '~' , ' '
};
// Initialisation
int const BufSize = 32;
uint8_t Buffer[BufSize];
int BufStart = 0, BufEnd = 0;
unsigned long receiveStart;
// You must connect the phone to this AP, then:
// menu -> connect -> Internet(TCP) -> 192.168.4.1:8880  for UART0
/*************************  COM Port 0 *******************************/
#define UART_BAUD0 9600            // Baudrate UART0
#define SERIAL_PARAM0 SERIAL_7E1    // Data/Parity/Stop UART0
#define SERIAL_PARAM1 SERIAL_8E1    // Data/Parity/Stop UART0
#define SERIAL0_TCP_PORT 8880       // Wifi Port UART0
#define bufferSize 1024
#define MAX_NMEA_CLIENTS 4
#define NUM_COM 1
#define DEBUG_COM 0 // debug output to COM0
//////////////////////////////////////////////////////////////////////////
enum UART_Mode { RECEIVE_MODE, TRANSMIT_MODE };
UART_Mode uartState[NUM_COM] = {TRANSMIT_MODE}; // начальное состояние
#include <WiFiClient.h>
WiFiServer server_0(SERIAL0_TCP_PORT);
WiFiServer *server[NUM_COM] = {&server_0};
WiFiClient TCPClient[NUM_COM][MAX_NMEA_CLIENTS];
HardwareSerial* COM[NUM_COM] = {&Serial};
uint8_t buf1[NUM_COM][bufferSize];
uint16_t i1[NUM_COM] = {0};
//9,10,12,13 чип ESP8266EX.
//GPIO14, GPIO12, GPIO13 и GPIO15
const int NewClient = 14; //New client
const int TRANSMIT = 2;
//const int Data = 12;
void setup() {
  pinMode(NewClient, OUTPUT);
  digitalWrite(NewClient, LOW);
  pinMode(TRANSMIT, OUTPUT);
  digitalWrite(TRANSMIT, LOW);
  //pinMode(Data, OUTPUT);
  COM[0]->begin(UART_BAUD0, SERIAL_PARAM1, SERIAL_TX_ONLY);
  if (debug) COM[DEBUG_COM]->println("\n\nLK8000 WiFi serial bridge V1.00");
  if (debug) COM[DEBUG_COM]->println("Open ESP Access Point mode");
  //AP mode (phone connects directly to ESP) (no router)
  WiFi.mode(WIFI_AP);
  WiFi.softAPConfig(ip, ip, netmask); // configure ip address for softAP
  WiFi.softAP(ssid, pw); // configure ssid and password for softAP
  if (debug) COM[DEBUG_COM]->println("Starting TCP Server 1");
  if (debug) COM[DEBUG_COM]->println("Starting TCP Server 1");
  server[0]->begin(); // start TCP server
  server[0]->setNoDelay(true);
}
void loop()
{
  for (int num = 0; num < NUM_COM ; num++)
  {
    if (server[num]->hasClient())
    {
      for (byte i = 0; i < MAX_NMEA_CLIENTS; i++) {
        if (!TCPClient[num][i] || !TCPClient[num][i].connected()) {
          if (TCPClient[num][i]) TCPClient[num][i].stop();
          TCPClient[num][i] = server[num]->available();
          continue;
        }
      }
      WiFiClient TmpserverClient = server[num]->available();
      TmpserverClient.stop();
    }
  }
  for (int num = 0; num < NUM_COM ; num++)
  {
    if (COM[num] != NULL)
    {
      for (byte cln = 0; cln < MAX_NMEA_CLIENTS; cln++)
      {
        if (TCPClient[num][cln])
        {
          if (uartState[num] == RECEIVE_MODE) {
            while (COM[num]->available()) {
              uint8_t inChar = COM[num]->read();
              if (inChar != -1) {
                char c = (char)inChar & 0x7F;
                if (c >= 32) {
                  TCPClient[num][cln].print(CharMap[c - 32]);
                  COM[num]->flush();
                } else if (c == '\r' || c == '\n') {
                  TCPClient[num][cln].print(c);
                  COM[num]->flush();
                }
                if (c == '@' || c == '#') {
                  uartState[num] = TRANSMIT_MODE;
                  break;
                }
              }
            }
            // Таймаут: переключаемся в TRANSMIT только если
            // клиент что-то прислал. Иначе ждём дальше.
            if (uartState[num] == RECEIVE_MODE
                && (millis() - receiveStart > 60000)
                && TCPClient[num][cln].available() > 0) {
              uartState[num] = TRANSMIT_MODE;
            }
            if (uartState[num] == TRANSMIT_MODE) {
              COM[num]->end();
              delay(10);
              COM[num]->begin(UART_BAUD0, SERIAL_PARAM1, SERIAL_TX_ONLY);
              delay(50);
            }
          }
          if (uartState[num] == TRANSMIT_MODE) {
            digitalWrite(TRANSMIT, HIGH);
            clearBuffer(num);
            delay(50);
            while (TCPClient[num][cln].available())
            {
              digitalWrite(NewClient, HIGH);
              buf1[num][i1[num]] = TCPClient[num][cln].read();
              TCPClient[num][cln].print(char(buf1[num][i1[num]]));
              if (i1[num] < bufferSize - 1) {
                i1[num]++;
              }
            }
            if (i1[num] > 0) {
              COM[num]->write(buf1[num], i1[num]);
              COM[num]->flush();
              i1[num] = 0;
              digitalWrite(NewClient, LOW);
              uartState[num] = RECEIVE_MODE;
              COM[num]->end();
              delay(10);
              digitalWrite(TRANSMIT, LOW);
              COM[num]->begin(UART_BAUD0, SERIAL_PARAM0, SERIAL_RX_ONLY);
              delay(50);
              receiveStart = millis();
            }
          }
        }
      }
    }
  }
}


void clearBuffer(int num) {
  for (int i = 0; i < bufferSize; i++) {
    buf1[num][i] = 0;
  }
  i1[num] = 0;
}
void putBuf (uint8_t byt) {
  if ((BufStart + BufSize - BufEnd) % BufSize == 1) return; // Buffer full
  Buffer[BufEnd] = byt;
  BufEnd = (BufEnd + 1) % BufSize;
}
char getBuf () {
  if (BufStart == BufEnd) return -1;                    // Buffer empty
  char temp = Buffer[BufStart];
  BufStart = (BufStart + 1) % BufSize;
  return temp;
}
 

alexlaw

✩✩✩✩✩✩✩
3 Янв 2020
113
2
Воронеж
Java:
package com.example.touchChess;

import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;

public class MainActivity extends Activity {
    private static Context sInstance;
    private int h = 60;
    private int dx = 26;
    private int dy = 22;
    private int touchX, touchY;
    private int XPol, YPol;

    private int selectedX = -1, selectedY = -1;
    private boolean hasSelection = false;

    private static final int STATE_IDLE = 0;
    private static final int STATE_WAITING_CONFIRMATION = 1;
    private static final int STATE_WAITING_ENGINE = 2;
    private int serverState = STATE_IDLE;

    private static final int SPECIAL_NONE = 0;
    private static final int SPECIAL_CASTLING = 1;
    private static final int SPECIAL_EN_PASSANT = 2;
    private int pendingSpecial = SPECIAL_NONE;
    private String pendingMove = "";
    private int pendingFromNum = -1;

    private Bitmap bitmapBackup;
    private Bitmap[] arrayOfBitmap = new Bitmap[12];
    private Bitmap bitmap1, bitmap2, bitmap3, bitmap4, bitmap5;
    private Canvas canvasBmp3, canvasBmp5;
    private Paint paint;
    private DrawView drawView;
    private String StrFen;

    private Socket mSocket;
    private InputStream mInputBuffer;
    private OutputStream mOutputBuffer;
    private Thread mReaderThread;
    private volatile boolean mIsReading = false;
    private String mServerPort = "8880";
    private String mServerAddress = "192.168.4.1";

    private TextView mTextViewContent;
    private ScrollView mScrollViewContent;
    private Button mButtonConnect;
    private Button mButtonSend;
    private EditText mEditTextCommand;

    public static Context getInstance() { return sInstance; }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        sInstance = this;
        setContentView(R.layout.activity_main);
        StrFen = getString(R.string.perft0);
        FrameLayout container = findViewById(R.id.boardContainer);
        drawView = new DrawView(this);
        container.addView(drawView);
        mScrollViewContent = findViewById(R.id.scrollViewContent);
        mTextViewContent = findViewById(R.id.textViewContent);
        mButtonConnect = findViewById(R.id.buttonConnect);
        mButtonConnect.requestFocus();
        mButtonSend = findViewById(R.id.buttonSend);
        mEditTextCommand = findViewById(R.id.editTextCommand);
        mButtonConnect.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) { new ConnectTask().execute(); }
        });
        mButtonSend.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String cmd = mEditTextCommand.getText().toString().trim();
                if (cmd.isEmpty()) { ShowAlert("Введите команду"); return; }
                new SendTask().execute(cmd);
            }
        });
    }

    // --- Нотация ---
    private String squareToNotation(int numPol) {
        int file = numPol % 8;
        int rank = numPol / 8;
        return "" + (char) ('a' + file) + (char) ('1' + rank);
    }
    private int notationToSquare(String notation) {
        int file = notation.charAt(0) - 'a';
        int rank = notation.charAt(1) - '1';
        return rank * 8 + file;
    }
    private int[] squareToCell(int square) {
        int file = square % 8;
        int rank = square / 8;
        return new int[]{file, 7 - rank};
    }
    private String extractMoveFromLine(String line) {
        int movIdx = line.indexOf("mov ");
        if (movIdx < 0) return null;
        int start = movIdx + 4;
        if (start + 4 > line.length()) return null;
        return line.substring(start, start + 4);
    }

    // --- Распознавание фигур ---

    // Распознаёт фигуру в bitmap5 (не модифицирует)
    private int identifyPiece() {
        if (!bitmap5HasContent()) return -1;
        int bestMatch = -1;
        int bestScore = 0;
        for (int i = 0; i < 12; i++) {
            int score = compareBitmaps(bitmap5, arrayOfBitmap[i]);
            if (score > bestScore) { bestScore = score; bestMatch = i; }
        }
        int[] refPixels = new int[60 * 60];
        int idx = (bestMatch >= 0) ? bestMatch : 0;
        arrayOfBitmap[idx].getPixels(refPixels, 0, 60, 0, 0, 60, 60);
        int refTotal = 0;
        for (int p : refPixels) { if (p != Color.TRANSPARENT) refTotal++; }
        if (refTotal > 0 && bestScore >= refTotal * 3 / 4) return bestMatch;
        return -1;
    }

    private int compareBitmaps(Bitmap a, Bitmap b) {
        int[] pa = new int[60 * 60];
        int[] pb = new int[60 * 60];
        a.getPixels(pa, 0, 60, 0, 0, 60, 60);
        b.getPixels(pb, 0, 60, 0, 0, 60, 60);
        int match = 0;
        for (int i = 0; i < pa.length; i++) {
            boolean aOp = pa[i] != Color.TRANSPARENT;
            boolean bOp = pb[i] != Color.TRANSPARENT;
            if (aOp && bOp) { if (pa[i] == pb[i]) match++; }
            else if (!aOp && !bOp) { match++; }
        }
        return match;
    }

    // Распознаёт фигуру в клетке bitmap3, НЕ трогая bitmap5
    private int identifyPieceAtSafe(int cellX, int cellY) {
        int srcX = dx + h * cellX;
        int srcY = dy + h * cellY;
        if (srcX < 0 || srcY < 0 || srcX + 60 > bitmap3.getWidth() || srcY + 60 > bitmap3.getHeight()) return -1;

        int[] content = new int[60 * 60];
        int[] boardBg = new int[60 * 60];
        bitmap3.getPixels(content, 0, 60, srcX, srcY, 60, 60);
        bitmap1.getPixels(boardBg, 0, 60, srcX, srcY, 60, 60);

        // Убираем фон
        for (int i = 0; i < content.length; i++) {
            if (content[i] == boardBg[i]) content[i] = Color.TRANSPARENT;
        }

        int bestMatch = -1;
        int bestScore = 0;
        for (int i = 0; i < 12; i++) {
            int[] ref = new int[60 * 60];
            arrayOfBitmap[i].getPixels(ref, 0, 60, 0, 0, 60, 60);
            int match = 0;
            int refTotal = 0;
            for (int j = 0; j < content.length; j++) {
                boolean cOp = content[j] != Color.TRANSPARENT;
                boolean rOp = ref[j] != Color.TRANSPARENT;
                if (cOp && rOp) {
                    refTotal++;
                    if (content[j] == ref[j]) match++;
                } else if (!cOp && !rOp) {
                    refTotal++;
                    match++;
                }
            }
            if (match > bestScore) { bestScore = match; bestMatch = i; }
        }

        int[] refPixels = new int[60 * 60];
        int idx = (bestMatch >= 0) ? bestMatch : 0;
        arrayOfBitmap[idx].getPixels(refPixels, 0, 60, 0, 0, 60, 60);
        int refTotal = 0;
        for (int p : refPixels) { if (p != Color.TRANSPARENT) refTotal++; }
        if (refTotal > 0 && bestScore >= refTotal * 3 / 4) return bestMatch;
        return -1;
    }

    // --- Проверка пустоты ---
    private boolean isCellEmpty(int cellX, int cellY) {
        int srcX = dx + h * cellX;
        int srcY = dy + h * cellY;
        if (srcX < 0 || srcY < 0 || srcX + 60 > bitmap3.getWidth() || srcY + 60 > bitmap3.getHeight()) return true;
        int[] content = new int[60 * 60];
        int[] board = new int[60 * 60];
        bitmap3.getPixels(content, 0, 60, srcX, srcY, 60, 60);
        bitmap1.getPixels(board, 0, 60, srcX, srcY, 60, 60);
        for (int i = 0; i < content.length; i++) {
            if (content[i] != board[i]) return false;
        }
        return true;
    }

    // --- Перемещение ---
    private void movePiece(int fromX, int fromY, int toX, int toY) {
        extractFigure(fromX, fromY);
        clearCell(fromX, fromY);
        clearCell(toX, toY);
        canvasBmp3.drawBitmap(bitmap5, dx + h * toX, dy + h * toY, null);
        canvasBmp5.drawColor(Color.TRANSPARENT);
    }

    // --- Применение хода движка (чёрные) ---
    private void applyMove(String move) {
        if (move == null || move.length() != 4) return;
        int fromSquare = notationToSquare(move.substring(0, 2));
        int toSquare = notationToSquare(move.substring(2, 4));
        int fromFile = fromSquare % 8;
        int fromRank = fromSquare / 8;
        int toFile = toSquare % 8;
        int toRank = toSquare / 8;
        int[] fromCell = squareToCell(fromSquare);
        int[] toCell = squareToCell(toSquare);

        // Определяем тип фигуры ДО перемещения
        int pieceType = identifyPieceAtSafe(fromCell[0], fromCell[1]);

        boolean isCastling = (pieceType == 6
            && Math.abs(toFile - fromFile) == 2 && fromRank == toRank);

        boolean isEnPassant = (pieceType == 11
            && fromFile != toFile && fromRank == 3
            && isCellEmpty(toCell[0], toCell[1]));

        boolean isPromotion = (pieceType == 11
            && toRank == 0);

        if (isCastling) {
            movePiece(fromCell[0], fromCell[1], toCell[0], toCell[1]);
            int rookFromNum = (toSquare > fromSquare) ? fromSquare + 3 : fromSquare - 4;
            int rookToNum = (toSquare > fromSquare) ? fromSquare + 1 : fromSquare - 1;
            int[] rfc = squareToCell(rookFromNum);
            int[] rtc = squareToCell(rookToNum);
            movePiece(rfc[0], rfc[1], rtc[0], rtc[1]);
        } else if (isEnPassant) {
            extractFigure(fromCell[0], fromCell[1]);
            clearCell(fromCell[0], fromCell[1]);
            clearCell(toCell[0], fromCell[1]);
            canvasBmp3.drawBitmap(bitmap5, dx + h * toCell[0], dy + h * toCell[1], null);
            canvasBmp5.drawColor(Color.TRANSPARENT);
        } else if (isPromotion) {
            clearCell(fromCell[0], fromCell[1]);
            clearCell(toCell[0], toCell[1]);
            canvasBmp3.drawBitmap(arrayOfBitmap[7], dx + h * toCell[0], dy + h * toCell[1], null);
            canvasBmp5.drawColor(Color.TRANSPARENT);
        } else {
            movePiece(fromCell[0], fromCell[1], toCell[0], toCell[1]);
        }
    }

    // --- Анализ ответа сервера ---
    private void checkServerResponse() {
        String fullText = mTextViewContent.getText().toString();
        if (serverState == STATE_WAITING_CONFIRMATION) {
            int hashIdx = fullText.lastIndexOf("#");
            int movIdx = fullText.lastIndexOf("mov ");
            if (movIdx >= 0) {
                int lineEnd = fullText.indexOf('\n', movIdx);
                if (lineEnd < 0) lineEnd = fullText.length();
                String movLine = fullText.substring(movIdx, lineEnd).trim();
                if (movLine.contains("w@")) {
                    serverState = STATE_WAITING_ENGINE;
                    mTextViewContent.setText("");
                    mEditTextCommand.setText("0");
                    new SendTask().execute("0");
                    return;
                }
            }
            if (hashIdx >= 0) {
                serverState = STATE_IDLE;
                mTextViewContent.setText("");
                if (bitmapBackup != null) canvasBmp3.drawBitmap(bitmapBackup, 0, 0, null);
                if (drawView != null) drawView.invalidate();
                ShowAlert("Нелегальный ход!");
                return;
            }
        } else if (serverState == STATE_WAITING_ENGINE) {
            int movIdx = fullText.lastIndexOf("mov ");
            if (movIdx >= 0) {
                int lineEnd = fullText.indexOf('\n', movIdx);
                if (lineEnd < 0) lineEnd = fullText.length();
                String movLine = fullText.substring(movIdx, lineEnd).trim();
                if (movLine.contains("b@")) {
                    serverState = STATE_IDLE;
                    mTextViewContent.setText("");
                    String move = extractMoveFromLine(movLine);
                    if (move != null) applyMove(move);
                    if (drawView != null) drawView.invalidate();
                    return;
                }
            }
        }
    }

    // --- Сеть ---
    private boolean isConnected() {
        return mSocket != null && mSocket.isConnected() && !mSocket.isClosed();
    }
    private class ConnectTask extends AsyncTask<Void, Void, Boolean> {
        private String errorMessage = null;
        @Override
        protected Boolean doInBackground(Void... arg0) {
            if (!isConnected()) {
                try {
                    mSocket = new Socket();
                    mSocket.connect(new InetSocketAddress(mServerAddress, Integer.parseInt(mServerPort)), 5000);
                    mInputBuffer = mSocket.getInputStream();
                    mOutputBuffer = mSocket.getOutputStream();
                    startReaderThread();
                    return true;
                } catch (Exception e) {
                    errorMessage = e.getClass().getName() + ": " + (e.getMessage() != null ? e.getMessage() : "no message");
                    return false;
                }
            } else {
                stopReaderThread();
                closeSocket();
                return false;
            }
        }
        @Override
        protected void onPostExecute(Boolean result) {
            updateConnectionStatus(result);
            if (errorMessage != null) ShowAlert(errorMessage);
        }
    }
    private class SendTask extends AsyncTask<String, Void, Boolean> {
        private String errorMessage = null;
        @Override
        protected Boolean doInBackground(String... params) {
            if (!isConnected()) { errorMessage = "Нет соединения"; return false; }
            try {
                mOutputBuffer.write((params[0] + "X").getBytes("UTF-8"));
                mOutputBuffer.flush();
                return true;
            } catch (Exception e) {
                errorMessage = e.getClass().getName() + ": " + (e.getMessage() != null ? e.getMessage() : "no message");
                return false;
            }
        }
        @Override
        protected void onPostExecute(Boolean result) {
            if (!result && errorMessage != null) ShowAlert(errorMessage);
        }
    }
    private void startReaderThread() {
        mIsReading = true;
        mReaderThread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (mIsReading && mInputBuffer != null) {
                    try {
                        int available = mInputBuffer.available();
                        if (available > 0) {
                            byte[] buffer = new byte[available];
                            int bytesRead = mInputBuffer.read(buffer);
                            if (bytesRead > 0) {
                                final String s = new String(buffer, 0, bytesRead, "UTF-8")
                                    .replace("\r", "").replaceAll("\n+", "\n");
                                runOnUiThread(new Runnable() {
                                    @Override
                                    public void run() {
                                        if (mTextViewContent != null) mTextViewContent.append(s);
                                        if (serverState != STATE_IDLE) checkServerResponse();
                                    }
                                });
                                mScrollViewContent.post(new Runnable() {
                                    @Override
                                    public void run() { mScrollViewContent.fullScroll(View.FOCUS_DOWN); }
                                });
                            }
                        } else { Thread.sleep(50); }
                    } catch (IOException | InterruptedException e) { break; }
                }
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        if (serverState != STATE_IDLE) {
                            serverState = STATE_IDLE;
                            if (bitmapBackup != null) canvasBmp3.drawBitmap(bitmapBackup, 0, 0, null);
                            if (drawView != null) drawView.invalidate();
                            ShowAlert("Соединение потеряно");
                        }
                        mButtonConnect.setText(R.string.stringConnect);
                    }
                });
            }
        });
        mReaderThread.start();
    }
    private void stopReaderThread() {
        mIsReading = false;
        if (mReaderThread != null) {
            mReaderThread.interrupt();
            try { mReaderThread.join(1000); } catch (InterruptedException ignored) {}
            mReaderThread = null;
        }
    }
    private void closeSocket() {
        try { if (mSocket != null && !mSocket.isClosed()) mSocket.close(); } catch (IOException ignored) {}
        mSocket = null;
        mInputBuffer = null;
        mOutputBuffer = null;
    }
    private void updateConnectionStatus(boolean connected) {
        mButtonConnect.setText(connected ? R.string.stringDisconnect : R.string.stringConnect);
    }

    // --- Шахматы ---
    private void prepareBoard() {
        canvasBmp3.drawBitmap(bitmap1, 0, 0, null);
        parsingStrFen(StrFen, canvasBmp3);
    }
    private void clearCell(int cellX, int cellY) {
        int x = dx + h * cellX;
        int y = dy + h * cellY;
        if (x < 0 || y < 0 || x + 60 > bitmap3.getWidth() || y + 60 > bitmap3.getHeight()) return;
        canvasBmp3.drawBitmap(bitmap1, new Rect(x, y, x + 60, y + 60), new Rect(x, y, x + 60, y + 60), null);
    }
    private void extractFigure(int cellX, int cellY) {
        int srcX = dx + h * cellX;
        int srcY = dy + h * cellY;
        if (srcX < 0 || srcY < 0 || srcX + 60 > bitmap3.getWidth() || srcY + 60 > bitmap3.getHeight()) return;
        canvasBmp5.drawColor(Color.TRANSPARENT);
        canvasBmp5.drawBitmap(bitmap3, new Rect(srcX, srcY, srcX + 60, srcY + 60), new Rect(0, 0, 60, 60), null);
        Bitmap cellMask = Bitmap.createBitmap(bitmap1, srcX, srcY, 60, 60);
        int[] pixels = new int[60 * 60];
        bitmap5.getPixels(pixels, 0, 60, 0, 0, 60, 60);
        int[] maskPixels = new int[60 * 60];
        cellMask.getPixels(maskPixels, 0, 60, 0, 0, 60, 60);
        for (int i = 0; i < pixels.length; i++) {
            if (pixels[i] == maskPixels[i]) pixels[i] = Color.TRANSPARENT;
        }
        bitmap5.setPixels(pixels, 0, 60, 0, 0, 60, 60);
    }
    private boolean bitmap5HasContent() {
        int[] pixels = new int[60 * 60];
        bitmap5.getPixels(pixels, 0, 60, 0, 0, 60, 60);
        for (int p : pixels) { if (p != Color.TRANSPARENT) return true; }
        return false;
    }
    private void initPiece() {
        for (int i = 0; i < 6; i++) {
            arrayOfBitmap[i] = Bitmap.createBitmap(bitmap2, i * 60, 0, 60, 60);
            arrayOfBitmap[i + 6] = Bitmap.createBitmap(bitmap2, i * 60, 60, 60, 60);
        }
    }
    public boolean parsingStrFen(String str, Canvas canvas) {
        String s1 = "KQRBNPkqrbnp";
        String s2 = "12345678";
        int n = 0, k = 0, king = 0;
        int r = str.indexOf(' ');
        if (r <= 0) return false;
        String sub = str.substring(0, r);
        for (int i = 0; i < sub.length(); i++) {
            String s = sub.substring(i, i + 1);
            if (s.equals("/")) {
                if (n != 8) ShowAlert("ERROR");
                n = 0; k++; continue;
            }
            if (s2.indexOf(s) >= 0) n += 1 + s2.indexOf(s);
            int z = s1.indexOf(s);
            if (z >= 0) {
                if (z == 0) king++;
                if (z == 6) king--;
                canvas.drawBitmap(arrayOfBitmap[z], dx + h * n, dy + h * k, paint);
                n++;
            }
        }
        if (king != 0) ShowAlert("ERROR");
        return true;
    }

    // --- Диалоги ---
    public static void showAlert(Context context, String title, String message, View view) {
        AlertDialog.Builder builder = new AlertDialog.Builder(context);
        builder.setTitle(title);
        if (view == null) builder.setMessage(message);
        else builder.setView(view);
        builder.setNeutralButton("Ok", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); }
        });
        builder.show();
    }
    public void ShowAlert(String str) {
        Toast.makeText(getApplicationContext(), str, Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onBackPressed() {
        stopReaderThread();
        closeSocket();
        mButtonConnect.setText(R.string.stringConnect);
        mTextViewContent.setText(R.string.waiting_esp);
        mEditTextCommand.setText("");
        serverState = STATE_IDLE;
        pendingSpecial = SPECIAL_NONE;
        pendingMove = "";
        pendingFromNum = -1;
        hasSelection = false;
        selectedX = -1;
        selectedY = -1;
        ShowAlert("Выход");
        super.onBackPressed();
    }

    @Override
    protected void onDestroy() {
        stopReaderThread();
        closeSocket();
        if (bitmapBackup != null) { bitmapBackup.recycle(); bitmapBackup = null; }
        if (bitmap1 != null) { bitmap1.recycle(); bitmap1 = null; }
        if (bitmap2 != null) { bitmap2.recycle(); bitmap2 = null; }
        if (bitmap3 != null) { bitmap3.recycle(); bitmap3 = null; }
        if (bitmap4 != null) { bitmap4.recycle(); bitmap4 = null; }
        if (bitmap5 != null) { bitmap5.recycle(); bitmap5 = null; }
        for (int i = 0; i < 12; i++) {
            if (arrayOfBitmap[i] != null) { arrayOfBitmap[i].recycle(); arrayOfBitmap[i] = null; }
        }
        super.onDestroy();
    }
 

alexlaw

✩✩✩✩✩✩✩
3 Янв 2020
113
2
Воронеж
Java:
   // --- Отрисовка ---
    class DrawView extends View {
        public DrawView(Context context) {
            super(context);
            paint = new Paint(Paint.ANTI_ALIAS_FLAG);
            bitmap1 = BitmapFactory.decodeResource(getResources(), R.drawable.board);
            bitmap2 = BitmapFactory.decodeResource(getResources(), R.drawable.peice);
            bitmap4 = Bitmap.createBitmap(bitmap2, 360, 0, 60, 60);
            bitmap3 = Bitmap.createBitmap(bitmap1.getWidth(), bitmap1.getHeight(), Bitmap.Config.ARGB_8888);
            bitmap5 = Bitmap.createBitmap(60, 60, Bitmap.Config.ARGB_8888);
            canvasBmp3 = new Canvas(bitmap3);
            canvasBmp5 = new Canvas(bitmap5);
            initPiece();
            prepareBoard();
        }

        @Override
        protected void onDraw(Canvas canvas) {
            canvas.drawARGB(80, 102, 204, 255);
            canvas.drawBitmap(bitmap3, 0, 0, paint);
            if (hasSelection) {
                canvas.drawBitmap(bitmap4, dx + h * selectedX, dy + h * selectedY, null);
            }
        }

        @Override
        public boolean onTouchEvent(MotionEvent event) {
            if (event.getAction() != MotionEvent.ACTION_DOWN) return true;
            if (serverState != STATE_IDLE) return true;

            touchX = (int) event.getX();
            touchY = (int) event.getY();
            if (touchX < dx || touchX > dx + 8 * h) return true;
            if (touchY < dy || touchY > dy + 8 * h) return true;

            XPol = (touchX - dx) / h;
            YPol = (touchY - dy) / h;

            // === ВТОРОЙ ХОД ОСОБОГО ХОДА ===
            if (pendingSpecial != SPECIAL_NONE) {
                if (!hasSelection) {
                    extractFigure(XPol, YPol);
                    if (bitmap5HasContent()) {
                        selectedX = XPol;
                        selectedY = YPol;
                        hasSelection = true;
                    } else {
                        ShowAlert("Клетка пуста");
                    }
                } else {
                    if (XPol == selectedX && YPol == selectedY) {
                        hasSelection = false;
                        selectedX = -1;
                        selectedY = -1;
                        canvasBmp5.drawColor(Color.TRANSPARENT);
                        ShowAlert("Снято выделение");
                    } else if (pendingSpecial == SPECIAL_CASTLING) {
                        movePiece(selectedX, selectedY, XPol, YPol);
                        hasSelection = false;
                        selectedX = -1;
                        selectedY = -1;
                        pendingSpecial = SPECIAL_NONE;
                        mEditTextCommand.setText(pendingMove);
                        if (isConnected()) {
                            serverState = STATE_WAITING_CONFIRMATION;
                            new SendTask().execute(pendingMove);
                        } else { ShowAlert("Нет соединения"); }
                    } else if (pendingSpecial == SPECIAL_EN_PASSANT) {
                        movePiece(selectedX, selectedY, XPol, YPol);
                        hasSelection = false;
                        selectedX = -1;
                        selectedY = -1;
                        pendingSpecial = SPECIAL_NONE;
                        int finalToNum = 63 - ((7 - XPol) + 8 * YPol);
                        String move = squareToNotation(pendingFromNum) + squareToNotation(finalToNum);
                        mEditTextCommand.setText(move);
                        if (isConnected()) {
                            serverState = STATE_WAITING_CONFIRMATION;
                            new SendTask().execute(move);
                        } else { ShowAlert("Нет соединения"); }
                    }
                }
                invalidate();
                return true;
            }

            // === ОБЫЧНАЯ ЛОГИКА ===
            if (!hasSelection) {
                extractFigure(XPol, YPol);
                if (bitmap5HasContent()) {
                    selectedX = XPol;
                    selectedY = YPol;
                    hasSelection = true;
                } else {
                    ShowAlert("Клетка пуста");
                }
            } else {
                if (XPol == selectedX && YPol == selectedY) {
                    hasSelection = false;
                    selectedX = -1;
                    selectedY = -1;
                    canvasBmp5.drawColor(Color.TRANSPARENT);
                    ShowAlert("Снято выделение");
                } else {
                    int fromNum = 63 - ((7 - selectedX) + 8 * selectedY);
                    int toNum   = 63 - ((7 - XPol) + 8 * YPol);
                    String move = squareToNotation(fromNum) + squareToNotation(toNum);

                    if (bitmapBackup != null) bitmapBackup.recycle();
                    bitmapBackup = bitmap3.copy(Bitmap.Config.ARGB_8888, true);

                    int fromFile = fromNum % 8;
                    int fromRank = fromNum / 8;
                    int toFile = toNum % 8;
                    int toRank = toNum / 8;

                    // Распознаём фигуру в bitmap5 (там уже вырезанная фигура)
                    int pieceType = identifyPiece();

                    // --- Рокировка: белый король (0) на 2 клетки ---
                    boolean isCastling = (pieceType == 0
                        && Math.abs(toFile - fromFile) == 2
                        && fromRank == toRank);

                    // --- Взятие на проходе: белая пешка (5), 5-я горизонталь,
                    // ход на соседнюю вертикаль по той же горизонтали,
                    // на целевой клетке — чёрная пешка (11) ---
                    boolean isEnPassant = (pieceType == 5
                        && Math.abs(toFile - fromFile) == 1
                        && fromRank == toRank
                        && fromRank == 4
                        && identifyPieceAtSafe(XPol, YPol) == 11);

                    // --- Превращение: белая пешка (5) на 8-ю горизонталь ---
                    boolean isPromotion = (pieceType == 5
                        && toRank == 7);

                    if (isCastling) {
                        int rookFromNum = (toNum > fromNum) ? fromNum + 3 : fromNum - 4;
                        int[] rookFromCell = squareToCell(rookFromNum);
                        int rookPiece = identifyPieceAtSafe(rookFromCell[0], rookFromCell[1]);

                        boolean pathClear = true;
                        if (toNum > fromNum) {
                            int[] s1 = squareToCell(fromNum + 1);
                            int[] s2 = squareToCell(fromNum + 2);
                            if (!isCellEmpty(s1[0], s1[1]) || !isCellEmpty(s2[0], s2[1])) pathClear = false;
                        } else {
                            int[] s1 = squareToCell(fromNum - 1);
                            int[] s2 = squareToCell(fromNum - 2);
                            int[] s3 = squareToCell(fromNum - 3);
                            if (!isCellEmpty(s1[0], s1[1]) || !isCellEmpty(s2[0], s2[1]) || !isCellEmpty(s3[0], s3[1])) pathClear = false;
                        }

                        if (rookPiece != 2 || !pathClear) {
                            canvasBmp3.drawBitmap(bitmapBackup, 0, 0, null);
                            ShowAlert("Условия рокировки не выполнены");
                            hasSelection = false;
                            selectedX = -1;
                            selectedY = -1;
                        } else {
                            clearCell(selectedX, selectedY);
                            clearCell(XPol, YPol);
                            canvasBmp3.drawBitmap(bitmap5, dx + h * XPol, dy + h * YPol, null);
                            canvasBmp5.drawColor(Color.TRANSPARENT);
                            pendingSpecial = SPECIAL_CASTLING;
                            pendingMove = move;
                            ShowAlert("Сделайте ход ладьёй");
                            hasSelection = false;
                            selectedX = -1;
                            selectedY = -1;
                        }
                    } else if (isEnPassant) {
                        clearCell(XPol, selectedY);
                        clearCell(selectedX, selectedY);
                        canvasBmp3.drawBitmap(bitmap5, dx + h * XPol, dy + h * selectedY, null);
                        canvasBmp5.drawColor(Color.TRANSPARENT);
                        pendingSpecial = SPECIAL_EN_PASSANT;
                        pendingFromNum = fromNum;
                        ShowAlert("Сделайте ход пешкой");
                        hasSelection = false;
                        selectedX = -1;
                        selectedY = -1;
                    } else if (isPromotion) {
                        clearCell(selectedX, selectedY);
                        clearCell(XPol, YPol);
                        canvasBmp3.drawBitmap(arrayOfBitmap[1], dx + h * XPol, dy + h * YPol, null);
                        canvasBmp5.drawColor(Color.TRANSPARENT);
                        hasSelection = false;
                        selectedX = -1;
                        selectedY = -1;
                        mEditTextCommand.setText(move);
                        if (isConnected()) {
                            serverState = STATE_WAITING_CONFIRMATION;
                            new SendTask().execute(move);
                        } else { ShowAlert("Нет соединения"); }
                    } else {
                        clearCell(selectedX, selectedY);
                        clearCell(XPol, YPol);
                        canvasBmp3.drawBitmap(bitmap5, dx + h * XPol, dy + h * YPol, null);
                        canvasBmp5.drawColor(Color.TRANSPARENT);
                        hasSelection = false;
                        selectedX = -1;
                        selectedY = -1;
                        mEditTextCommand.setText(move);
                        if (isConnected()) {
                            serverState = STATE_WAITING_CONFIRMATION;
                            new SendTask().execute(move);
                        } else { ShowAlert("Нет соединения"); }
                    }
                }
            }
            invalidate();
            return true;
        }
    }
}