
What you'll build
This guide takes you through building a playable touchscreen chess game on the ESP32-2432S028R, usually known as the Cheap Yellow Display. The board combines an ESP32, a 240×320 ILI9341 TFT, and an XPT2046 resistive touchscreen, so the hardware is mostly self-contained. You power it over USB, deploy the firmware, and play directly on the screen with the included stylus.
The firmware draws an 8×8 chess board, lets you select pieces by touch, highlights legal moves, and gives Black a small local opponent. It is not trying to be Stockfish. It is a compact embedded chess prototype that shows how much you can do when the display, touch controller, code, wiring notes, and deploy path are generated together.
By the end of the build you will have a working handheld chess prototype running on a single ESP32 display board. It is still a first version: the next obvious upgrades are battery power, a case, and maybe a lichess or chess.com connection. This project came from Sam's Schematik workshop build, which you can watch on YouTube or read as a build diary.
Wiring diagram
Wiring diagram
Components needed
| Component | Type | Qty | Buy |
|---|---|---|---|
| Built-in 240×320 ILI9341 TFT | display | 1 | €4.65 |
Prices and availability are indicative and may have been updated by the supplier. Schematik may earn a commission from purchases made through affiliate links.
Assembly
Review the hardware and gather your tools
This project uses a single ESP32 development board with a built-in ILI9341 240x320 TFT display hard-wired onboard. Because all display connections are already made on the PCB, no external wiring is required for the display itself. Before you begin, confirm you have the correct board in hand — it should have a visible TFT screen mounted directly on the board. Gather a USB cable compatible with your board (micro-USB or USB-C depending on the variant) and a computer running Chrome or Edge for Web Serial support via Schematik Deploy. No breadboard, jumper wires, or additional components are needed for this build.
- Boards with onboard TFT displays are often labeled as 'ESP32 with TFT' or 'ESP32-S2 TFT Feather' style boards — confirm your board model before proceeding.
- If you are using a Freenove ESP32 kit or similar, check the product page to verify the TFT is genuinely hard-wired and not on a separate breakout module.
- Use Chrome or Edge browser — Firefox does not support the Web Serial API needed by Schematik Deploy.
- Do not attempt to reassign display pins (MOSI GPIO13, MISO GPIO12, SCK GPIO14, CS GPIO15, DC GPIO2, BL GPIO21) in firmware — they are hard-wired on the PCB and cannot be changed without hardware modification.
- GPIO2 is shared between the DC (Data/Command) signal for the TFT and the built-in LED on many ESP32 boards. Be aware that toggling GPIO2 in your sketch will affect both.
Understand the onboard display wiring
The built-in ILI9341 TFT is connected internally to the ESP32 HSPI bus. The fixed pin assignments are as follows: MOSI is on GPIO13, MISO is on GPIO12, SCK (clock) is on GPIO14, CS (chip select) is on GPIO15, DC (data/command select) is on GPIO2, and BL (backlight, PWM-controlled) is on GPIO21. These connections exist entirely on the PCB — there is nothing for you to wire. Understanding these assignments is important because you will need to configure the TFT_eSPI library to match them exactly. Note that GPIO21 is also the default I2C SDA pin on ESP32; if you add I2C peripherals in a future revision, you will need to remap I2C or manage GPIO21 carefully.
- Write down or bookmark these pin assignments: MOSI=GPIO13, MISO=GPIO12, SCK=GPIO14, CS=GPIO15, DC=GPIO2, BL=GPIO21. You will need them when editing the TFT_eSPI User_Setup.h file.
- The backlight on GPIO21 uses PWM. Use ledcSetup(), ledcAttachPin(), and ledcWrite() in your sketch to control brightness — do not use analogWrite() on ESP32.
- GPIO12 is a strapping pin on the ESP32. At boot, it must be LOW for the standard 3.3V flash voltage. The onboard TFT holds this line, so do not add external pull-ups to GPIO12.
- GPIO6 through GPIO11 are reserved for internal flash and must never be used — they are unrelated to the display but worth noting as a general caution.
Power the board via USB
Connect your ESP32 board to your computer using the appropriate USB cable. The onboard voltage regulator converts the USB 5V supply to 3.3V for the ESP32 and the TFT display. The estimated total current draw for this project is approximately 300mA, which is well within USB 2.0 port limits. You do not need an external power supply. Once connected, the board should power on and you may see the display backlight illuminate if default firmware is present. Make sure the USB cable supports data transfer (not a charge-only cable), as you will need the data lines for firmware upload.
- If your computer's USB port seems unstable or the board resets unexpectedly during upload, try a powered USB hub or connect directly to a rear motherboard USB port rather than a front-panel port.
- A data-capable USB cable is required — many cheap cables are charge-only and will not work for programming.
- The ESP32 GPIO pins operate at 3.3V and are NOT 5V tolerant. Do not connect any 5V logic signals to GPIO pins, even though the board is powered from 5V USB.
- Peak current during Wi-Fi transmission can reach up to 500mA. If you enable Wi-Fi in your sketch, ensure your USB port or power source can handle brief current spikes.
Configure the TFT_eSPI library for the onboard display
Before deploying firmware, the TFT_eSPI library must be configured to match the onboard pin assignments. In your Arduino or PlatformIO environment, locate the TFT_eSPI library folder and open the User_Setup.h file. Uncomment or set the ILI9341_DRIVER define. Then set the following pin definitions: TFT_MOSI to 13, TFT_MISO to 12, TFT_SCLK to 14, TFT_CS to 15, TFT_DC to 2, and TFT_BL to 21. Set TFT_WIDTH to 240 and TFT_HEIGHT to 320. Enable the HSPI bus by setting USE_HSPI_PORT. If backlight control is desired, define TFT_BACKLIGHT_ON as HIGH. Save the file before proceeding to the Schematik Deploy step.
- Some ESP32 TFT boards ship with a pre-configured User_Setup.h or a board-specific User_Setup_Select.h entry. Check the TFT_eSPI GitHub repository for a matching setup file before editing manually.
- In Schematik, if the project was generated with TFT_eSPI as the listed library, the configuration may already be embedded in the generated sketch — verify before making manual changes.
- If you are using PlatformIO, pin definitions can also be passed via build flags in platformio.ini, keeping User_Setup.h unmodified.
- If the TFT_eSPI library is not configured correctly, the display will show nothing or produce garbled output. Double-check every pin number before uploading.
- Do not select the wrong driver — choosing ST7789 or ST7735 instead of ILI9341 will produce no output or a blank white screen.
Deploy the firmware using Schematik Deploy
With your board connected via USB and the browser open in Chrome or Edge, navigate to the Schematik Deploy panel. Select your ESP32 board model from the board selector dropdown to ensure the correct flash settings and partition scheme are used. Click the serial port selector and choose the COM port (Windows) or /dev/ttyUSB0 or /dev/cu.usbserial (Mac/Linux) that corresponds to your connected board. If no port appears, check that your USB cable is data-capable and that the board's USB-to-serial driver is installed (CP210x or CH340 depending on your board variant). Once the port is selected, click the Deploy button. Schematik will compile the sketch with the TFT_eSPI library and flash it to the board over Web Serial. Watch the progress bar and output log for any errors.
- If the upload fails at the start, hold the BOOT button on the ESP32 board just as the upload begins, then release it once the progress bar starts moving. Some boards require manual boot mode entry.
- On Mac, you may need to grant Chrome permission to access USB serial devices in System Preferences under Security and Privacy.
- After a successful flash, the board will automatically reset and begin running the new firmware.
- Only Chrome and Edge support the Web Serial API required by Schematik Deploy. Safari and Firefox will not work.
- Do not disconnect the USB cable during flashing — this can corrupt the firmware and require a manual recovery flash.
Verify the display and confirm successful operation
After flashing completes, the board will reset automatically. Within a few seconds, the TFT display should illuminate and show the application interface rendered by your sketch. The backlight on GPIO21 should be active. If your sketch includes a splash screen, UI elements, or game board, these should appear on the 240x320 pixel display. To further confirm correct operation, open the Schematik serial monitor (or your IDE's serial monitor) at 115200 baud. Look for initialization messages from the TFT_eSPI library such as 'TFT initialized' or any debug output your sketch produces. If the display remains blank but the backlight is on, re-check the TFT_eSPI pin configuration from the previous step and re-deploy. If the board does not boot at all, check that GPIO12 is not being pulled high externally.
- A successful first boot shows the display rendering content within 2 to 3 seconds of the board resetting after flash.
- If the image appears but colors are wrong (red and blue swapped), try toggling the TFT_RGB_ORDER setting in User_Setup.h between TFT_RGB and TFT_BGR.
- Use the serial monitor at 115200 baud to read any runtime debug output and confirm the sketch loop is running as expected.
- If GPIO2 is being driven by both the TFT DC signal and any LED blink code in your sketch, the display may show corruption during LED toggles. Remove any digitalWrite(2, ...) calls unrelated to the TFT from your sketch.
- The ESP32-S2 variant is single-core — if your sketch uses a computationally heavy algorithm such as Minimax AI, ensure it is depth-limited or runs in a background task so the display refresh loop stays responsive.
Pin assignments
| Pin | Connection | Type |
|---|---|---|
| GPIO 13 | onboard-tft_0 MOSI | SPI |
| GPIO 12 | onboard-tft_0 MISO | SPI |
| GPIO 14 | onboard-tft_0 SCK | SPI |
| GPIO 15 | onboard-tft_0 CS | SPI |
| GPIO 2 | onboard-tft_0 DC | DIGITAL |
| GPIO 21 | onboard-tft_0 BL | PWM |
Code
// Chess game for ESP32-2432S028R with ILI9341 TFT display
// Player (White) vs AI (Black) using minimax with alpha-beta pruning
// Touch screen for piece selection and movement
#include <SPI.h>
#include <TFT_eSPI.h>
#include <XPT2046_Touchscreen.h>
// Override loopTask stack size to 64KB. arduino-esp32 v2.0.17 declares
// `size_t getArduinoLoopTaskStackSize(void);` with C++ linkage in Arduino.h
// (not extern "C"), so our override must match that linkage exactly.
size_t getArduinoLoopTaskStackSize() {
return 64 * 1024;
}
// ─── Pin Definitions ───────────────────────────────────────────────────────
#define TFT_BL_PIN 21
#define TOUCH_CS_PIN 33
#define TOUCH_IRQ_PIN 36
#define TOUCH_MOSI 32
#define TOUCH_MISO 39
#define TOUCH_SCK 25
// ─── Display & Touch Objects ───────────────────────────────────────────────
// Hoisted type definitions
struct Move {
int fromRow, fromCol, toRow, toCol;
int capturedPiece, capturedColor;
bool promotion;
int promotedFrom;
bool enPassant;
bool castling;
// castling rook info
int rookFromCol, rookToCol;
};
struct GameState {
int board[8][8]; // piece type
int color[8][8]; // piece color
bool whiteCastleK; // white can castle kingside
bool whiteCastleQ; // white can castle queenside
bool blackCastleK;
bool blackCastleQ;
int enPassantCol; // column of en passant target (-1 if none)
int enPassantRow; // row of en passant target
int currentPlayer; // WHITE_PIECE or BLACK_PIECE
};
// Forward declarations
void initBoard(GameState &gs);
void drawStatus(const char *msg);
void drawGameOver(const char *msg);
void drawPiece(int row, int col, int piece, int pieceColor);
void drawSquare(int row, int col, bool highlight, bool moveDot, GameState &gs);
void drawBoard(GameState &gs);
bool inBounds(int r, int c);
bool squareAttacked(GameState &gs, int row, int col, int byPlayer);
bool isInCheck(GameState &gs, int player);
void addMove(Move *moves, int &count, int fr, int fc, int tr, int tc, GameState &gs, bool enPassant = false, bool castling = false, int rookFromCol = -1, int rookToCol = -1);
void generatePseudoMoves(GameState &gs, int player, Move *moves, int &count);
void applyMove(GameState &gs, Move &m);
void undoMove(GameState &gs, Move &m);
void generateMoves(GameState &gs, int player, Move *moves, int &count);
bool isCheckmate(GameState &gs, int player);
bool isStalemate(GameState &gs, int player);
int pieceValue(int piece);
int getPST(int piece, int row, int col, int color);
int evaluateBoard(GameState &gs);
int minimax(GameState &gs, int depth, int alpha, int beta, bool maximizing);
Move getBestMove(GameState &gs);
void getTouchSquare(int &row, int &col);
TFT_eSPI tft = TFT_eSPI();
SPIClass touchSPI(VSPI);
XPT2046_Touchscreen touch(TOUCH_CS_PIN, TOUCH_IRQ_PIN);
// ─── Chess Constants ───────────────────────────────────────────────────────
#define BOARD_SIZE 8
#define SQUARE_SIZE 30
#define BOARD_OFFSET_X 0
#define BOARD_OFFSET_Y 40
// Piece types
#define EMPTY 0
#define PAWN 1
#define KNIGHT 2
#define BISHOP 3
#define ROOK 4
#define QUEEN 5
#define KING 6
// Colors
#define WHITE_PIECE 1
#define BLACK_PIECE -1
// TFT Colors
#define COLOR_LIGHT_SQ 0xFFE0
#define COLOR_DARK_SQ 0x6B4D
#define COLOR_SELECTED 0x07E0
#define COLOR_MOVE_DOT 0x07FF
#define COLOR_BG 0x0000
#define COLOR_WHITE_P 0xFFFF
#define COLOR_BLACK_P 0x18C3
#define COLOR_TEXT 0xFFFF
#define COLOR_STATUS_BG 0x2104
// ─── Game State Structs ────────────────────────────────────────────────────
// ─── Forward Declarations ──────────────────────────────────────────────────
void initBoard(GameState &gs);
void drawBoard(GameState &gs);
void drawSquare(int row, int col, bool highlight, bool moveDot, GameState &gs);
void drawPiece(int row, int col, int piece, int pieceColor);
bool isValidMove(GameState &gs, Move &m);
void generateMoves(GameState &gs, int player, Move *moves, int &count);
void applyMove(GameState &gs, Move &m);
void undoMove(GameState &gs, Move &m);
bool isInCheck(GameState &gs, int player);
bool isCheckmate(GameState &gs, int player);
bool isStalemate(GameState &gs, int player);
int evaluateBoard(GameState &gs);
int minimax(GameState &gs, int depth, int alpha, int beta, bool maximizing);
Move getBestMove(GameState &gs);
void getTouchSquare(int &row, int &col);
void drawStatus(const char *msg);
void drawGameOver(const char *msg);
bool squareAttacked(GameState &gs, int row, int col, int byPlayer);
void handlePromotion(GameState &gs, int row, int col, int pieceColor);
// ─── Globals ───────────────────────────────────────────────────────────────
GameState gs;
int selectedRow = -1;
int selectedCol = -1;
bool pieceSelected = false;
Move legalMoves[256];
int legalMoveCount = 0;
bool gameOver = false;
char statusMsg[64] = "Your turn (White)";
// Touch calibration values (may need tuning)
#define TOUCH_X_MIN 200
#define TOUCH_X_MAX 3800
#define TOUCH_Y_MIN 300
#define TOUCH_Y_MAX 3700
// ─── Board Initialization ──────────────────────────────────────────────────
void initBoard(GameState &gs) {
memset(gs.board, 0, sizeof(gs.board));
memset(gs.color, 0, sizeof(gs.color));
// Back rows
int backRow[8] = {ROOK, KNIGHT, BISHOP, QUEEN, KING, BISHOP, KNIGHT, ROOK};
for (int c = 0; c < 8; c++) {
// Black back row (row 0)
gs.board[0][c] = backRow[c];
gs.color[0][c] = BLACK_PIECE;
// Black pawns (row 1)
gs.board[1][c] = PAWN;
gs.color[1][c] = BLACK_PIECE;
// White pawns (row 6)
gs.board[6][c] = PAWN;
gs.color[6][c] = WHITE_PIECE;
// White back row (row 7)
gs.board[7][c] = backRow[c];
gs.color[7][c] = WHITE_PIECE;
}
gs.whiteCastleK = true;
gs.whiteCastleQ = true;
gs.blackCastleK = true;
gs.blackCastleQ = true;
gs.enPassantCol = -1;
gs.enPassantRow = -1;
gs.currentPlayer = WHITE_PIECE;
}
// ─── Drawing Functions ─────────────────────────────────────────────────────
void drawStatus(const char *msg) {
tft.fillRect(0, 0, 240, 38, COLOR_STATUS_BG);
tft.setTextColor(COLOR_TEXT, COLOR_STATUS_BG);
tft.setTextSize(1);
tft.setCursor(4, 4);
tft.print(msg);
}
void drawGameOver(const char *msg) {
tft.fillRect(20, 100, 200, 60, TFT_RED);
tft.setTextColor(TFT_WHITE, TFT_RED);
tft.setTextSize(2);
tft.setCursor(30, 115);
tft.print(msg);
tft.setTextSize(1);
tft.setCursor(50, 140);
tft.print("Tap to restart");
}
void drawPiece(int row, int col, int piece, int pieceColor) {
int x = BOARD_OFFSET_X + col * SQUARE_SIZE + SQUARE_SIZE / 2;
int y = BOARD_OFFSET_Y + row * SQUARE_SIZE + SQUARE_SIZE / 2;
uint16_t fg = (pieceColor == WHITE_PIECE) ? COLOR_WHITE_P : COLOR_BLACK_P;
uint16_t outline = (pieceColor == WHITE_PIECE) ? TFT_BLACK : TFT_WHITE;
int r = SQUARE_SIZE / 2 - 4;
switch (piece) {
case PAWN:
tft.fillCircle(x, y + 3, r - 3, fg);
tft.drawCircle(x, y + 3, r - 3, outline);
tft.fillRect(x - 4, y + 7, 9, 3, fg);
tft.drawRect(x - 4, y + 7, 9, 3, outline);
break;
case ROOK:
tft.fillRect(x - r + 2, y - r + 4, (r - 2) * 2, (r) * 2 - 2, fg);
tft.drawRect(x - r + 2, y - r + 4, (r - 2) * 2, (r) * 2 - 2, outline);
tft.fillRect(x - r + 2, y - r, 3, 5, fg);
tft.fillRect(x - 1, y - r, 3, 5, fg);
tft.fillRect(x + r - 5, y - r, 3, 5, fg);
tft.drawRect(x - r + 2, y - r, 3, 5, outline);
tft.drawRect(x - 1, y - r, 3, 5, outline);
tft.drawRect(x + r - 5, y - r, 3, 5, outline);
break;
case KNIGHT:
tft.fillCircle(x + 2, y - 2, r - 2, fg);
tft.drawCircle(x + 2, y - 2, r - 2, outline);
tft.fillTriangle(x - r + 2, y + r - 2, x + r - 2, y + r - 2, x - 2, y, fg);
tft.drawTriangle(x - r + 2, y + r - 2, x + r - 2, y + r - 2, x - 2, y, outline);
break;
case BISHOP:
tft.fillTriangle(x, y - r + 1, x - r + 3, y + r - 2, x + r - 3, y + r - 2, fg);
tft.drawTriangle(x, y - r + 1, x - r + 3, y + r - 2, x + r - 3, y + r - 2, outline);
tft.fillCircle(x, y - r + 2, 3, fg);
tft.drawCircle(x, y - r + 2, 3, outline);
break;
case QUEEN:
tft.fillCircle(x, y, r - 1, fg);
tft.drawCircle(x, y, r - 1, outline);
// Crown points
for (int i = -2; i <= 2; i++) {
tft.fillCircle(x + i * 4, y - r + 1, 2, fg);
tft.drawCircle(x + i * 4, y - r + 1, 2, outline);
}
tft.fillCircle(x, y, 3, outline);
break;
case KING:
tft.fillCircle(x, y + 2, r - 2, fg);
tft.drawCircle(x, y + 2, r - 2, outline);
// Cross on top
tft.fillRect(x - 1, y - r, 3, 8, fg);
tft.fillRect(x - 4, y - r + 3, 9, 3, fg);
tft.drawRect(x - 1, y - r, 3, 8, outline);
tft.drawRect(x - 4, y - r + 3, 9, 3, outline);
break;
default:
break;
}
}
void drawSquare(int row, int col, bool highlight, bool moveDot, GameState &gs) {
int x = BOARD_OFFSET_X + col * SQUARE_SIZE;
int y = BOARD_OFFSET_Y + row * SQUARE_SIZE;
uint16_t bg;
if (highlight) {
bg = COLOR_SELECTED;
} else {
bg = ((row + col) % 2 == 0) ? COLOR_LIGHT_SQ : COLOR_DARK_SQ;
}
tft.fillRect(x, y, SQUARE_SIZE, SQUARE_SIZE, bg);
if (moveDot && gs.board[row][col] == EMPTY) {
tft.fillCircle(x + SQUARE_SIZE / 2, y + SQUARE_SIZE / 2, 4, COLOR_MOVE_DOT);
} else if (moveDot && gs.board[row][col] != EMPTY) {
// Highlight capture
tft.drawRect(x, y, SQUARE_SIZE, SQUARE_SIZE, COLOR_MOVE_DOT);
tft.drawRect(x + 1, y + 1, SQUARE_SIZE - 2, SQUARE_SIZE - 2, COLOR_MOVE_DOT);
}
if (gs.board[row][col] != EMPTY) {
drawPiece(row, col, gs.board[row][col], gs.color[row][col]);
}
}
// New Game button — bottom of screen, below the chess board.
// 240×320 portrait: board occupies y=40..280; column labels at y=282..290;
// button gets the remaining strip y=294..318.
#define NEW_GAME_BTN_X 60
#define NEW_GAME_BTN_Y 294
#define NEW_GAME_BTN_W 120
#define NEW_GAME_BTN_H 24
void drawNewGameButton() {
tft.fillRoundRect(NEW_GAME_BTN_X, NEW_GAME_BTN_Y, NEW_GAME_BTN_W, NEW_GAME_BTN_H, 4, TFT_DARKGREEN);
tft.drawRoundRect(NEW_GAME_BTN_X, NEW_GAME_BTN_Y, NEW_GAME_BTN_W, NEW_GAME_BTN_H, 4, TFT_GREEN);
tft.setTextSize(1);
tft.setTextColor(TFT_WHITE, TFT_DARKGREEN);
// Centred-ish text — TFT_eSPI default font is ~6 pixels per character.
tft.setCursor(NEW_GAME_BTN_X + (NEW_GAME_BTN_W - 8 * 6) / 2, NEW_GAME_BTN_Y + (NEW_GAME_BTN_H - 8) / 2);
tft.print("NEW GAME");
}
bool isTouchOnNewGameButton() {
if (!touch.touched()) return false;
TS_Point p = touch.getPoint();
int tx = map(p.x, TOUCH_X_MIN, TOUCH_X_MAX, 0, 240);
int ty = map(p.y, TOUCH_Y_MIN, TOUCH_Y_MAX, 0, 320);
return (tx >= NEW_GAME_BTN_X && tx < NEW_GAME_BTN_X + NEW_GAME_BTN_W &&
ty >= NEW_GAME_BTN_Y && ty < NEW_GAME_BTN_Y + NEW_GAME_BTN_H);
}
void resetGame() {
initBoard(gs);
pieceSelected = false;
selectedRow = -1;
selectedCol = -1;
legalMoveCount = 0;
gameOver = false;
tft.fillScreen(COLOR_BG);
drawBoard(gs);
drawNewGameButton();
drawStatus("Your turn (White)");
}
void drawBoard(GameState &gs) {
tft.fillRect(BOARD_OFFSET_X, BOARD_OFFSET_Y, 8 * SQUARE_SIZE, 8 * SQUARE_SIZE, COLOR_LIGHT_SQ);
// Determine if any square is selected and build move dots
bool dotSquare[8][8];
memset(dotSquare, false, sizeof(dotSquare));
if (pieceSelected) {
for (int i = 0; i < legalMoveCount; i++) {
dotSquare[legalMoves[i].toRow][legalMoves[i].toCol] = true;
}
}
for (int r = 0; r < 8; r++) {
for (int c = 0; c < 8; c++) {
bool highlight = (pieceSelected && r == selectedRow && c == selectedCol);
drawSquare(r, c, highlight, dotSquare[r][c], gs);
}
}
// Draw coordinates
tft.setTextSize(1);
tft.setTextColor(TFT_YELLOW, COLOR_BG);
for (int c = 0; c < 8; c++) {
tft.setCursor(BOARD_OFFSET_X + c * SQUARE_SIZE + 12, BOARD_OFFSET_Y + 8 * SQUARE_SIZE + 2);
tft.print((char)('a' + c));
}
for (int r = 0; r < 8; r++) {
tft.setCursor(BOARD_OFFSET_X + 8 * SQUARE_SIZE + 2, BOARD_OFFSET_Y + r * SQUARE_SIZE + 10);
tft.print(8 - r);
}
}
// ─── Move Validation Helpers ───────────────────────────────────────────────
bool inBounds(int r, int c) {
return r >= 0 && r < 8 && c >= 0 && c < 8;
}
bool squareAttacked(GameState &gs, int row, int col, int byPlayer) {
// Check if (row,col) is attacked by 'byPlayer'
// Pawn attacks
int pawnDir = (byPlayer == WHITE_PIECE) ? 1 : -1;
int pawnRow = row + pawnDir;
if (inBounds(pawnRow, col - 1) && gs.board[pawnRow][col - 1] == PAWN && gs.color[pawnRow][col - 1] == byPlayer) return true;
if (inBounds(pawnRow, col + 1) && gs.board[pawnRow][col + 1] == PAWN && gs.color[pawnRow][col + 1] == byPlayer) return true;
// Knight attacks
int kd[8][2] = {{-2,-1},{-2,1},{-1,-2},{-1,2},{1,-2},{1,2},{2,-1},{2,1}};
for (int i = 0; i < 8; i++) {
int nr = row + kd[i][0], nc = col + kd[i][1];
if (inBounds(nr, nc) && gs.board[nr][nc] == KNIGHT && gs.color[nr][nc] == byPlayer) return true;
}
// Sliding pieces (Bishop/Queen diagonals)
int diagD[4][2] = {{1,1},{1,-1},{-1,1},{-1,-1}};
for (int d = 0; d < 4; d++) {
int nr = row + diagD[d][0], nc = col + diagD[d][1];
while (inBounds(nr, nc)) {
if (gs.board[nr][nc] != EMPTY) {
if (gs.color[nr][nc] == byPlayer && (gs.board[nr][nc] == BISHOP || gs.board[nr][nc] == QUEEN)) return true;
break;
}
nr += diagD[d][0]; nc += diagD[d][1];
}
}
// Sliding pieces (Rook/Queen straight)
int straightD[4][2] = {{1,0},{-1,0},{0,1},{0,-1}};
for (int d = 0; d < 4; d++) {
int nr = row + straightD[d][0], nc = col + straightD[d][1];
while (inBounds(nr, nc)) {
if (gs.board[nr][nc] != EMPTY) {
if (gs.color[nr][nc] == byPlayer && (gs.board[nr][nc] == ROOK || gs.board[nr][nc] == QUEEN)) return true;
break;
}
nr += straightD[d][0]; nc += straightD[d][1];
}
}
// King
for (int dr = -1; dr <= 1; dr++) {
for (int dc = -1; dc <= 1; dc++) {
if (dr == 0 && dc == 0) continue;
int nr = row + dr, nc = col + dc;
if (inBounds(nr, nc) && gs.board[nr][nc] == KING && gs.color[nr][nc] == byPlayer) return true;
}
}
return false;
}
bool isInCheck(GameState &gs, int player) {
int kingRow = -1, kingCol = -1;
for (int r = 0; r < 8 && kingRow == -1; r++) {
for (int c = 0; c < 8 && kingRow == -1; c++) {
if (gs.board[r][c] == KING && gs.color[r][c] == player) {
kingRow = r; kingCol = c;
}
}
}
if (kingRow == -1) return false;
int opponent = -player;
return squareAttacked(gs, kingRow, kingCol, opponent);
}
// ─── Move Generation ───────────────────────────────────────────────────────
void addMove(Move *moves, int &count, int fr, int fc, int tr, int tc, GameState &gs,
bool enPassant, bool castling, int rookFromCol, int rookToCol) {
if (count >= 255) return;
Move m;
m.fromRow = fr; m.fromCol = fc;
m.toRow = tr; m.toCol = tc;
m.capturedPiece = gs.board[tr][tc];
m.capturedColor = gs.color[tr][tc];
m.promotion = false;
m.promotedFrom = gs.board[fr][fc];
m.enPassant = enPassant;
m.castling = castling;
m.rookFromCol = rookFromCol;
m.rookToCol = rookToCol;
// Pawn promotion
if (gs.board[fr][fc] == PAWN) {
if ((gs.color[fr][fc] == WHITE_PIECE && tr == 0) ||
(gs.color[fr][fc] == BLACK_PIECE && tr == 7)) {
m.promotion = true;
}
}
moves[count++] = m;
}
void generatePseudoMoves(GameState &gs, int player, Move *moves, int &count) {
int dir = (player == WHITE_PIECE) ? -1 : 1;
for (int r = 0; r < 8; r++) {
for (int c = 0; c < 8; c++) {
if (gs.board[r][c] == EMPTY || gs.color[r][c] != player) continue;
int piece = gs.board[r][c];
if (piece == PAWN) {
int nr = r + dir;
// Forward
if (inBounds(nr, c) && gs.board[nr][c] == EMPTY) {
addMove(moves, count, r, c, nr, c, gs);
// Double push from start
int startRow = (player == WHITE_PIECE) ? 6 : 1;
if (r == startRow && gs.board[nr + dir][c] == EMPTY) {
addMove(moves, count, r, c, nr + dir, c, gs);
}
}
// Captures
for (int dc = -1; dc <= 1; dc += 2) {
int nc = c + dc;
if (!inBounds(nr, nc)) continue;
if (gs.board[nr][nc] != EMPTY && gs.color[nr][nc] != player) {
addMove(moves, count, r, c, nr, nc, gs);
}
// En passant
if (gs.enPassantCol == nc && gs.enPassantRow == nr) {
addMove(moves, count, r, c, nr, nc, gs, true);
}
}
}
else if (piece == KNIGHT) {
int kd[8][2] = {{-2,-1},{-2,1},{-1,-2},{-1,2},{1,-2},{1,2},{2,-1},{2,1}};
for (int i = 0; i < 8; i++) {
int nr = r + kd[i][0], nc = c + kd[i][1];
if (!inBounds(nr, nc)) continue;
if (gs.board[nr][nc] == EMPTY || gs.color[nr][nc] != player) {
addMove(moves, count, r, c, nr, nc, gs);
}
}
}
else if (piece == BISHOP || piece == QUEEN) {
int diagD[4][2] = {{1,1},{1,-1},{-1,1},{-1,-1}};
for (int d = 0; d < 4; d++) {
int nr = r + diagD[d][0], nc = c + diagD[d][1];
while (inBounds(nr, nc)) {
if (gs.board[nr][nc] != EMPTY) {
if (gs.color[nr][nc] != player) addMove(moves, count, r, c, nr, nc, gs);
break;
}
addMove(moves, count, r, c, nr, nc, gs);
nr += diagD[d][0]; nc += diagD[d][1];
}
}
}
if (piece == ROOK || piece == QUEEN) {
int straightD[4][2] = {{1,0},{-1,0},{0,1},{0,-1}};
for (int d = 0; d < 4; d++) {
int nr = r + straightD[d][0], nc = c + straightD[d][1];
while (inBounds(nr, nc)) {
if (gs.board[nr][nc] != EMPTY) {
if (gs.color[nr][nc] != player) addMove(moves, count, r, c, nr, nc, gs);
break;
}
addMove(moves, count, r, c, nr, nc, gs);
nr += straightD[d][0]; nc += straightD[d][1];
}
}
}
else if (piece == KING) {
for (int dr = -1; dr <= 1; dr++) {
for (int dc = -1; dc <= 1; dc++) {
if (dr == 0 && dc == 0) continue;
int nr = r + dr, nc = c + dc;
if (!inBounds(nr, nc)) continue;
if (gs.board[nr][nc] == EMPTY || gs.color[nr][nc] != player) {
addMove(moves, count, r, c, nr, nc, gs);
}
}
}
// Castling
int backRow = (player == WHITE_PIECE) ? 7 : 0;
int opponent = -player;
if (r == backRow && c == 4 && !isInCheck(gs, player)) {
// Kingside
bool canK = (player == WHITE_PIECE) ? gs.whiteCastleK : gs.blackCastleK;
if (canK && gs.board[backRow][5] == EMPTY && gs.board[backRow][6] == EMPTY &&
!squareAttacked(gs, backRow, 5, opponent) && !squareAttacked(gs, backRow, 6, opponent)) {
addMove(moves, count, r, c, backRow, 6, gs, false, true, 7, 5);
}
// Queenside
bool canQ = (player == WHITE_PIECE) ? gs.whiteCastleQ : gs.blackCastleQ;
if (canQ && gs.board[backRow][3] == EMPTY && gs.board[backRow][2] == EMPTY && gs.board[backRow][1] == EMPTY &&
!squareAttacked(gs, backRow, 3, opponent) && !squareAttacked(gs, backRow, 2, opponent)) {
addMove(moves, count, r, c, backRow, 2, gs, false, true, 0, 3);
}
}
}
}
}
}
void applyMove(GameState &gs, Move &m) {
int piece = gs.board[m.fromRow][m.fromCol];
int pieceColor = gs.color[m.fromRow][m.fromCol];
// Handle en passant capture
if (m.enPassant) {
int captureRow = m.fromRow;
gs.board[captureRow][m.toCol] = EMPTY;
gs.color[captureRow][m.toCol] = 0;
}
// Set new en passant target
gs.enPassantCol = -1;
gs.enPassantRow = -1;
if (piece == PAWN && abs(m.toRow - m.fromRow) == 2) {
gs.enPassantRow = (m.fromRow + m.toRow) / 2;
gs.enPassantCol = m.fromCol;
}
// Move piece
gs.board[m.toRow][m.toCol] = piece;
gs.color[m.toRow][m.toCol] = pieceColor;
gs.board[m.fromRow][m.fromCol] = EMPTY;
gs.color[m.fromRow][m.fromCol] = 0;
// Promotion
if (m.promotion) {
gs.board[m.toRow][m.toCol] = QUEEN;
}
// Castling: move rook
if (m.castling) {
int backRow = m.fromRow;
gs.board[backRow][m.rookToCol] = ROOK;
gs.color[backRow][m.rookToCol] = pieceColor;
gs.board[backRow][m.rookFromCol] = EMPTY;
gs.color[backRow][m.rookFromCol] = 0;
}
// Update castling rights
if (piece == KING) {
if (pieceColor == WHITE_PIECE) { gs.whiteCastleK = false; gs.whiteCastleQ = false; }
else { gs.blackCastleK = false; gs.blackCastleQ = false; }
}
if (piece == ROOK) {
if (pieceColor == WHITE_PIECE) {
if (m.fromCol == 7) gs.whiteCastleK = false;
if (m.fromCol == 0) gs.whiteCastleQ = false;
} else {
if (m.fromCol == 7) gs.blackCastleK = false;
if (m.fromCol == 0) gs.blackCastleQ = false;
}
}
gs.currentPlayer = -gs.currentPlayer;
}
void undoMove(GameState &gs, Move &m) {
int piece = gs.board[m.toRow][m.toCol];
int pieceColor = gs.color[m.toRow][m.toCol];
// Undo promotion
if (m.promotion) {
piece = PAWN;
}
gs.board[m.fromRow][m.fromCol] = piece;
gs.color[m.fromRow][m.fromCol] = pieceColor;
gs.board[m.toRow][m.toCol] = m.capturedPiece;
gs.color[m.toRow][m.toCol] = m.capturedColor;
// Undo en passant
if (m.enPassant) {
int captureRow = m.fromRow;
int captureColor = -pieceColor;
gs.board[captureRow][m.toCol] = PAWN;
gs.color[captureRow][m.toCol] = captureColor;
}
// Undo castling: move rook back
if (m.castling) {
int backRow = m.fromRow;
gs.board[backRow][m.rookFromCol] = ROOK;
gs.color[backRow][m.rookFromCol] = pieceColor;
gs.board[backRow][m.rookToCol] = EMPTY;
gs.color[backRow][m.rookToCol] = 0;
}
gs.currentPlayer = -gs.currentPlayer;
}
void generateMoves(GameState &gs, int player, Move *moves, int &count) {
count = 0;
Move pseudoMoves[256];
int pseudoCount = 0;
generatePseudoMoves(gs, player, pseudoMoves, pseudoCount);
// Snapshot/restore the full GameState rather than relying on undoMove —
// undoMove only restores board[][] / color[][], leaving enPassant target,
// castling rights, and currentPlayer corrupted. With those leaking across
// pseudoMove iterations, the AI sees positions that can't actually arise
// and emits illegal moves (e.g. rook capturing its own pawn). Full copy
// costs ~520 bytes per iteration but eliminates the whole class of bugs.
for (int i = 0; i < pseudoCount; i++) {
GameState saved = gs;
applyMove(gs, pseudoMoves[i]);
if (!isInCheck(gs, player)) {
moves[count++] = pseudoMoves[i];
}
gs = saved;
}
}
bool isCheckmate(GameState &gs, int player) {
Move moves[256];
int count = 0;
generateMoves(gs, player, moves, count);
return (count == 0 && isInCheck(gs, player));
}
bool isStalemate(GameState &gs, int player) {
Move moves[256];
int count = 0;
generateMoves(gs, player, moves, count);
return (count == 0 && !isInCheck(gs, player));
}
// ─── Evaluation ───────────────────────────────────────────────────────────
int pieceValue(int piece) {
switch (piece) {
case PAWN: return 100;
case KNIGHT: return 320;
case BISHOP: return 330;
case ROOK: return 500;
case QUEEN: return 900;
case KING: return 20000;
default: return 0;
}
}
// Piece-square tables (from white's perspective)
const int pawnTable[8][8] = {
{ 0, 0, 0, 0, 0, 0, 0, 0},
{50, 50, 50, 50, 50, 50, 50, 50},
{10, 10, 20, 30, 30, 20, 10, 10},
{ 5, 5, 10, 25, 25, 10, 5, 5},
{ 0, 0, 0, 20, 20, 0, 0, 0},
{ 5, -5,-10, 0, 0,-10, -5, 5},
{ 5, 10, 10,-20,-20, 10, 10, 5},
{ 0, 0, 0, 0, 0, 0, 0, 0}
};
const int knightTable[8][8] = {
{-50,-40,-30,-30,-30,-30,-40,-50},
{-40,-20, 0, 0, 0, 0,-20,-40},
{-30, 0, 10, 15, 15, 10, 0,-30},
{-30, 5, 15, 20, 20, 15, 5,-30},
{-30, 0, 15, 20, 20, 15, 0,-30},
{-30, 5, 10, 15, 15, 10, 5,-30},
{-40,-20, 0, 5, 5, 0,-20,-40},
{-50,-40,-30,-30,-30,-30,-40,-50}
};
const int bishopTable[8][8] = {
{-20,-10,-10,-10,-10,-10,-10,-20},
{-10, 0, 0, 0, 0, 0, 0,-10},
{-10, 0, 5, 10, 10, 5, 0,-10},
{-10, 5, 5, 10, 10, 5, 5,-10},
{-10, 0, 10, 10, 10, 10, 0,-10},
{-10, 10, 10, 10, 10, 10, 10,-10},
{-10, 5, 0, 0, 0, 0, 5,-10},
{-20,-10,-10,-10,-10,-10,-10,-20}
};
int getPST(int piece, int row, int col, int color) {
int r = (color == WHITE_PIECE) ? row : (7 - row);
switch (piece) {
case PAWN: return pawnTable[r][col];
case KNIGHT: return knightTable[r][col];
case BISHOP: return bishopTable[r][col];
default: return 0;
}
}
int evaluateBoard(GameState &gs) {
int score = 0;
for (int r = 0; r < 8; r++) {
for (int c = 0; c < 8; c++) {
if (gs.board[r][c] == EMPTY) continue;
int v = pieceValue(gs.board[r][c]) + getPST(gs.board[r][c], r, c, gs.color[r][c]);
if (gs.color[r][c] == BLACK_PIECE) score += v;
else score -= v;
}
}
return score;
}
// ─── Minimax with Alpha-Beta ───────────────────────────────────────────────
// Stack-overflow fix: a per-frame `Move moves[256]` (~10KB) × recursion depth
// blows past the loopTask stack (even at 32KB) when combined with the
// setup→loop→handleTouch→getBestMove call chain. Loop task is single-threaded
// so a per-depth global pool is safe — each recursion level reads/writes its
// own slot.
#define MINIMAX_MAX_DEPTH 6
static Move minimaxMoveBuf[MINIMAX_MAX_DEPTH + 1][256];
int minimax(GameState &gs, int depth, int alpha, int beta, bool maximizing) {
if (depth == 0) return evaluateBoard(gs);
int player = maximizing ? BLACK_PIECE : WHITE_PIECE;
Move *moves = (depth >= 0 && depth <= MINIMAX_MAX_DEPTH) ? minimaxMoveBuf[depth] : minimaxMoveBuf[0];
int count = 0;
generateMoves(gs, player, moves, count);
if (count == 0) {
if (isInCheck(gs, player)) {
return maximizing ? -30000 - depth : 30000 + depth;
}
return 0; // stalemate
}
// vTaskDelay(1) (= 1 tick) actually lets the IDLE task run, unlike yield()
// which only schedules equal-or-higher priority. Calling every node would
// be too slow (~1ms × thousands of nodes); call every 16th instead.
static uint32_t s_minimaxNodeCounter = 0;
if (maximizing) {
int maxEval = -32767;
for (int i = 0; i < count; i++) {
GameState saved = gs;
applyMove(gs, moves[i]);
int eval = minimax(gs, depth - 1, alpha, beta, false);
gs = saved;
if ((++s_minimaxNodeCounter & 0x0F) == 0) vTaskDelay(1);
if (eval > maxEval) maxEval = eval;
if (eval > alpha) alpha = eval;
if (beta <= alpha) break;
}
return maxEval;
} else {
int minEval = 32767;
for (int i = 0; i < count; i++) {
GameState saved = gs;
applyMove(gs, moves[i]);
int eval = minimax(gs, depth - 1, alpha, beta, true);
gs = saved;
if ((++s_minimaxNodeCounter & 0x0F) == 0) vTaskDelay(1);
if (eval < minEval) minEval = eval;
if (eval < beta) beta = eval;
if (beta <= alpha) break;
}
return minEval;
}
}
Move getBestMove(GameState &gs) {
Move moves[256];
int count = 0;
generateMoves(gs, BLACK_PIECE, moves, count);
Move bestMove = moves[0];
int bestVal = -32767;
int depth = 2; // Search depth — reduced from 3 to keep AI move time
// around 1s on ESP32 240MHz. Still a credible opponent.
for (int i = 0; i < count; i++) {
GameState saved = gs;
applyMove(gs, moves[i]);
int val = minimax(gs, depth - 1, -32767, 32767, false);
gs = saved;
if (val > bestVal) {
bestVal = val;
bestMove = moves[i];
}
}
return bestMove;
}
// ─── Touch Input ───────────────────────────────────────────────────────────
void getTouchSquare(int &row, int &col) {
row = -1; col = -1;
if (!touch.touched()) return;
TS_Point p = touch.getPoint();
// Map raw touch to screen coordinates
// The display is 240x320, touch is 240 wide x 320 tall
// Raw values need mapping based on calibration
int tx = map(p.x, TOUCH_X_MIN, TOUCH_X_MAX, 0, 240);
int ty = map(p.y, TOUCH_Y_MIN, TOUCH_Y_MAX, 0, 320);
tx = constrain(tx, 0, 239);
ty = constrain(ty, 0, 319);
// Convert to board square
int bx = tx - BOARD_OFFSET_X;
int by = ty - BOARD_OFFSET_Y;
if (bx < 0 || bx >= 8 * SQUARE_SIZE) return;
if (by < 0 || by >= 8 * SQUARE_SIZE) return;
col = bx / SQUARE_SIZE;
row = by / SQUARE_SIZE;
}
// ─── Setup & Loop ──────────────────────────────────────────────────────────
void setup() {
Serial.begin(115200);
// Backlight
ledcSetup(0, 5000, 8);
ledcAttachPin(TFT_BL_PIN, 0);
ledcWrite(0, 200);
// TFT
tft.init();
tft.setRotation(0); // Portrait
tft.fillScreen(COLOR_BG);
// Touch SPI
touchSPI.begin(TOUCH_SCK, TOUCH_MISO, TOUCH_MOSI, TOUCH_CS_PIN);
touch.begin(touchSPI);
touch.setRotation(0);
// Init game
initBoard(gs);
tft.fillScreen(COLOR_BG);
drawBoard(gs);
drawNewGameButton();
drawStatus("Your turn (White)");
// Diagnostic: how much loopTask stack is left at the end of setup()?
// Reports the minimum free stack (high-water mark). If this is small,
// setup() ate most of the stack and we need to bump getArduinoLoopTaskStackSize.
Serial.printf("[diag] loopTask stack high-water mark: %u bytes free\n",
(unsigned)uxTaskGetStackHighWaterMark(NULL));
Serial.println("Chess game started");
}
void loop() {
// ESP32 task watchdog feed: arduino-esp32 v2.x's loopTask doesn't yield
// between iterations, so a busy-waiting `loop()` (no touch → return early)
// starves Core 1 IDLE within ~5s and trips TG0WDT_SYS_RESET. delay(1)
// sleeps the loopTask for 1 tick, which is enough for IDLE to run.
// (yield() does NOT fix this — it only schedules equal-or-higher priority.)
delay(1);
// New Game button: works in any game state (mid-game, game over, AI's
// turn). Check before all other touch handling so a tap on the button
// always wins. Wait for finger release with the same 2s timeout used
// elsewhere so a stuck touch sensor can't freeze the reset.
if (isTouchOnNewGameButton()) {
delay(50); // debounce
if (isTouchOnNewGameButton()) {
unsigned long _waitStart = millis();
while (touch.touched() && millis() - _waitStart < 2000) { delay(10); }
resetGame();
return;
}
}
if (gameOver) {
// Wait for touch to restart
if (touch.touched()) {
delay(300);
// Wait for release — bail after 2s in case the sensor glitches and
// never reports !touched(). Without the timeout, a stuck touch event
// would freeze the game (loop() would never return, so we'd stop
// processing input even though the WDT stays fed by delay(10)).
unsigned long _waitStart = millis();
while (touch.touched() && millis() - _waitStart < 2000) { delay(10); }
gameOver = false;
pieceSelected = false;
selectedRow = -1;
selectedCol = -1;
legalMoveCount = 0;
initBoard(gs);
tft.fillScreen(COLOR_BG);
drawBoard(gs);
drawStatus("Your turn (White)");
}
return;
}
if (gs.currentPlayer == WHITE_PIECE) {
// Human's turn
if (!touch.touched()) return;
delay(50); // debounce
if (!touch.touched()) return;
int tRow, tCol;
getTouchSquare(tRow, tCol);
// Wait for release — same 2s timeout as the gameOver branch above.
unsigned long _waitStart = millis();
while (touch.touched() && millis() - _waitStart < 2000) { delay(10); }
if (tRow < 0 || tRow >= 8 || tCol < 0 || tCol >= 8) return;
if (!pieceSelected) {
// Select a piece
if (gs.board[tRow][tCol] != EMPTY && gs.color[tRow][tCol] == WHITE_PIECE) {
selectedRow = tRow;
selectedCol = tCol;
pieceSelected = true;
// Generate legal moves for this piece
Move allMoves[256];
int allCount = 0;
generateMoves(gs, WHITE_PIECE, allMoves, allCount);
legalMoveCount = 0;
for (int i = 0; i < allCount; i++) {
if (allMoves[i].fromRow == selectedRow && allMoves[i].fromCol == selectedCol) {
legalMoves[legalMoveCount++] = allMoves[i];
}
}
drawBoard(gs);
drawStatus("Select destination");
}
} else {
// Deselect if tapping same square
if (tRow == selectedRow && tCol == selectedCol) {
pieceSelected = false;
selectedRow = -1;
selectedCol = -1;
legalMoveCount = 0;
drawBoard(gs);
drawStatus("Your turn (White)");
return;
}
// Re-select another white piece
if (gs.board[tRow][tCol] != EMPTY && gs.color[tRow][tCol] == WHITE_PIECE) {
selectedRow = tRow;
selectedCol = tCol;
Move allMoves[256];
int allCount = 0;
generateMoves(gs, WHITE_PIECE, allMoves, allCount);
legalMoveCount = 0;
for (int i = 0; i < allCount; i++) {
if (allMoves[i].fromRow == selectedRow && allMoves[i].fromCol == selectedCol) {
legalMoves[legalMoveCount++] = allMoves[i];
}
}
drawBoard(gs);
drawStatus("Select destination");
return;
}
// Try to make a move
bool moveMade = false;
for (int i = 0; i < legalMoveCount; i++) {
if (legalMoves[i].toRow == tRow && legalMoves[i].toCol == tCol) {
applyMove(gs, legalMoves[i]);
moveMade = true;
break;
}
}
pieceSelected = false;
selectedRow = -1;
selectedCol = -1;
legalMoveCount = 0;
if (!moveMade) {
drawBoard(gs);
drawStatus("Invalid move!");
delay(800);
drawStatus("Your turn (White)");
return;
}
// Check game end conditions after player move
drawBoard(gs);
if (isCheckmate(gs, BLACK_PIECE)) {
drawStatus("Checkmate! You win!");
drawGameOver("You Win!");
gameOver = true;
return;
}
if (isStalemate(gs, BLACK_PIECE)) {
drawStatus("Stalemate!");
drawGameOver("Stalemate!");
gameOver = true;
return;
}
if (isInCheck(gs, BLACK_PIECE)) {
drawStatus("Check! AI thinking...");
} else {
drawStatus("AI thinking...");
}
}
} else {
// AI's turn (Black)
delay(100);
Move best = getBestMove(gs);
applyMove(gs, best);
drawBoard(gs);
if (isCheckmate(gs, WHITE_PIECE)) {
drawStatus("Checkmate! AI wins!");
drawGameOver("AI Wins!");
gameOver = true;
return;
}
if (isStalemate(gs, WHITE_PIECE)) {
drawStatus("Stalemate!");
drawGameOver("Stalemate!");
gameOver = true;
return;
}
if (isInCheck(gs, WHITE_PIECE)) {
drawStatus("You're in Check!");
} else {
drawStatus("Your turn (White)");
}
}
}
// Run this and build other cool things at schematik.ioReady to build this?
Open this project in Schematik to get the full wiring diagram, pin assignments, and deployable code for the Touchscreen Chess Game.
Open in Schematik →