В этой части мы завершаем разработку игры «Крестики-нолики» для Arduino UNO с использованием OLED SSD1306 и джойстика HW-504.
Теперь у нас есть:
OLED SSD1306 (I2C):
Джойстик HW-504:

Перед компиляцией нужно установить:
Установка: Arduino IDE → Скетч → Подключить библиотеку → Управлять библиотеками…
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
// ---------- Настройки OLED ----------
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_ADDR 0x3C
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
// ---------- Джойстик ----------
#define JOY_X A0
#define JOY_Y A1
#define JOY_SW 2
int JOY_X_CENTER = 512, JOY_Y_CENTER = 512;
const int DEADZONE = 180;
// ---------- Геометрия интерфейса ----------
const int GRID_SIZE = 64; // размер поля 64x64
const int CELL = 21; // 3 клетки (21*3=63), крайняя рамка добирает до 64
const int GRID_X = SCREEN_WIDTH - GRID_SIZE; // поле справа (128-64=64)
const int GRID_Y = 0; // по вертикали от верха
int cursorX = 0, cursorY = 0;
char board[3][3];
bool playerTurn = true;
bool vsComputer = false;
int menuPos = 0;
// ---------- Счет ----------
int scoreX = 0;
int scoreO = 0;
// ---------- Долгое нажатие ----------
unsigned long pressStart = 0;
bool pressed = false;
// ---------- Прото ----------
char checkWin();
bool canWin(char symbol, int &bestX, int &bestY);
void computerMove();
void showMenu();
void drawBoard();
void resetBoard();
void waitForButtonRelease();
// ---------- Сброс игры ----------
void resetBoard() {
for (int y = 0; y < 3; y++) {
for (int x = 0; x < 3; x++) board[y][x] = ' ';
}
cursorX = 0;
cursorY = 0;
playerTurn = true;
}
// ---------- Ждем отпускание кнопки (чтобы меню не «проскочило») ----------
void waitForButtonRelease() {
while (!digitalRead(JOY_SW)) { delay(10); } // пока нажата (LOW)
delay(150); // антидребезг
}
// ---------- Меню выбора режима (теперь не выходит сразу, ждет нормального клика) ----------
void showMenu() {
bool chosen = false;
bool allowSelect = false; // станет true после первого отпускания внутри меню
// Перед входом в меню дождемся отпускания, если держали кнопку
waitForButtonRelease();
while (!chosen) {
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(WHITE);
display.setCursor(14, 8);
display.println("Select mode:");
display.setCursor(18, 28);
if (menuPos == 0) display.print("> ");
display.println("2 players");
display.setCursor(18, 44);
if (menuPos == 1) display.print("> ");
display.println("VS machine");
display.display();
int y = analogRead(JOY_Y);
bool sw = !digitalRead(JOY_SW);
if (y < JOY_Y_CENTER - DEADZONE) { menuPos = max(0, menuPos - 1); delay(200); }
if (y > JOY_Y_CENTER + DEADZONE) { menuPos = min(1, menuPos + 1); delay(200); }
if (!sw) allowSelect = true; // кнопка отпущена — можно принимать выбор
if (sw && allowSelect) { // нажали ПОСЛЕ отпускания — выбираем
vsComputer = (menuPos == 1);
chosen = true;
waitForButtonRelease(); // дождаться отпускания перед выходом
}
}
}
// ---------- Рисуем поле + левую панель ----------
void drawBoard() {
display.clearDisplay();
// Левая колонка (под требования "в столбик")
display.setTextSize(1);
display.setCursor(2, 6); display.println("WIN");
display.setCursor(2, 22); display.print("X:"); display.println(scoreX);
display.setCursor(2, 38); display.print("O:"); display.println(scoreO);
// Внешняя рамка поля ровно 64x64 справа
display.drawRect(GRID_X, GRID_Y, GRID_SIZE, GRID_SIZE, WHITE);
// Внутренние линии строго в пределах 64x64
// Вертикальные:
display.drawLine(GRID_X + CELL, GRID_Y, GRID_X + CELL, GRID_Y + GRID_SIZE - 1, WHITE);
display.drawLine(GRID_X + CELL * 2, GRID_Y, GRID_X + CELL * 2, GRID_Y + GRID_SIZE - 1, WHITE);
// Горизонтальные:
display.drawLine(GRID_X, GRID_Y + CELL, GRID_X + GRID_SIZE - 1, GRID_Y + CELL, WHITE);
display.drawLine(GRID_X, GRID_Y + CELL * 2, GRID_X + GRID_SIZE - 1, GRID_Y + CELL * 2, WHITE);
// Крестики / нолики
for (int y = 0; y < 3; y++) {
for (int x = 0; x < 3; x++) {
int cx = GRID_X + x * CELL + CELL / 2; // центр клетки
int cy = GRID_Y + y * CELL + CELL / 2;
if (board[y][x] == 'X') {
display.drawLine(cx - 7, cy - 7, cx + 7, cy + 7, WHITE);
display.drawLine(cx - 7, cy + 7, cx + 7, cy - 7, WHITE);
} else if (board[y][x] == 'O') {
display.drawCircle(cx, cy, 8, WHITE);
}
}
}
// Курсор
int rx = GRID_X + cursorX * CELL;
int ry = GRID_Y + cursorY * CELL;
display.drawRect(rx, ry, CELL, CELL, WHITE);
display.display();
}
// ---------- Проверка победы ----------
char checkWin() {
for (int i = 0; i < 3; i++) {
if (board[i][0] != ' ' &&
board[i][0] == board[i][1] &&
board[i][1] == board[i][2]) return board[i][0];
if (board[0][i] != ' ' &&
board[0][i] == board[1][i] &&
board[1][i] == board[2][i]) return board[0][i];
}
if (board[0][0] != ' ' &&
board[0][0] == board[1][1] &&
board[1][1] == board[2][2]) return board[0][0];
if (board[0][2] != ' ' &&
board[0][2] == board[1][1] &&
board[1][1] == board[2][0]) return board[0][2];
return ' ';
}
// ---------- Проверка «могу выиграть этим ходом» ----------
bool canWin(char symbol, int &bestX, int &bestY) {
for (int y = 0; y < 3; y++) {
for (int x = 0; x < 3; x++) {
if (board[y][x] == ' ') {
board[y][x] = symbol;
if (checkWin() == symbol) {
bestX = x; bestY = y;
board[y][x] = ' ';
return true;
}
board[y][x] = ' ';
}
}
}
return false;
}
// ---------- Ход компьютера (умный: выиграть / блок / рандом) ----------
void computerMove() {
int bx = -1, by = -1;
if (canWin('O', bx, by)) { board[by][bx] = 'O'; return; }
if (canWin('X', bx, by)) { board[by][bx] = 'O'; return; }
int freeCells[9][2]; int count = 0;
for (int y = 0; y < 3; y++) {
for (int x = 0; x < 3; x++) {
if (board[y][x] == ' ') {
freeCells[count][0] = x;
freeCells[count][1] = y;
count++;
}
}
}
if (count > 0) {
int r = random(count);
board[freeCells[r][1]][freeCells[r][0]] = 'O';
}
}
// ---------- Инициализация ----------
void setup() {
pinMode(JOY_SW, INPUT_PULLUP);
randomSeed(analogRead(A3));
if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) {
for (;;);
}
showMenu();
resetBoard();
drawBoard();
}
// ---------- Основной цикл ----------
void loop() {
int x = analogRead(JOY_X);
int y = analogRead(JOY_Y);
bool sw = !digitalRead(JOY_SW); // LOW=нажата -> true
// Долгое нажатие -> уходим в меню и ЖДЕМ выбора
if (sw && !pressed) { pressed = true; pressStart = millis(); }
if (!sw && pressed) { pressed = false; }
if (pressed && (millis() - pressStart >= 2000)) {
// сбросим флаг и корректно войдем в меню
pressed = false;
waitForButtonRelease();
showMenu();
resetBoard();
drawBoard();
return; // важный выход: не продолжаем этот кадр игры
}
// Движение курсора
if (x < JOY_X_CENTER - DEADZONE) { cursorX = max(0, cursorX - 1); delay(200); }
if (x > JOY_X_CENTER + DEADZONE) { cursorX = min(2, cursorX + 1); delay(200); }
if (y < JOY_Y_CENTER - DEADZONE) { cursorY = max(0, cursorY - 1); delay(200); }
if (y > JOY_Y_CENTER + DEADZONE) { cursorY = min(2, cursorY + 1); delay(200); }
// Ход игрока (X)
if (sw && board[cursorY][cursorX] == ' ' && playerTurn) {
board[cursorY][cursorX] = 'X';
playerTurn = false;
delay(250);
}
// Ход компьютера или второго игрока
if (vsComputer && !playerTurn) {
computerMove();
playerTurn = true;
} else if (!vsComputer && !playerTurn && sw && board[cursorY][cursorX] == ' ') {
board[cursorY][cursorX] = 'O';
playerTurn = true;
delay(250);
}
// Проверка победы
char winner = checkWin();
if (winner != ' ') {
if (winner == 'X') scoreX++;
if (winner == 'O') scoreO++;
display.clearDisplay();
display.setTextSize(2);
display.setTextColor(WHITE);
display.setCursor(40, 18);
display.print(winner); display.print(" wins!");
display.setTextSize(1);
display.setCursor(8, 48);
display.print("Score X:"); display.print(scoreX);
display.print(" O:"); display.print(scoreO);
display.display();
delay(2500);
resetBoard();
}
drawBoard();
}
🔄 Сделайте ничью
Добавьте проверку на «ничью» — если все клетки заняты, но победителя нет, выводите на экран сообщение «Draw!» и обнуляйте поле.
🎨 Измените оформление
Попробуйте изменить шрифты, толщину линий, размеры крестиков и ноликов. Сделайте поле или панель счета другим стилем.
🔊 Звуковое сопровождение
Подключите пьезо-динамик к Arduino (например, на пин D8) и добавьте звуки: короткий «пик» при выборе клетки, более длинный сигнал при победе.
📈 Расширьте поле
Попробуйте сделать не поле 3×3, а 4×4 или даже 5×5. Для этого нужно изменить сетку и алгоритм проверки победы.
🤖 Улучшите интеллект компьютера
Сейчас компьютер только блокирует или ставит случайно. Попробуйте реализовать стратегию «Минимакс» или добавить приоритет: ходить в центр → углы → остальные клетки.
📊 Счет до победы
Сделайте систему игры до 3 побед подряд. Например, на экране пишите: «Best of 3» и фиксируйте чемпиона матча.
💾 Сохраняйте счет в EEPROM
Реализуйте сохранение счета в энергонезависимую память EEPROM, чтобы после перезапуска Arduino счет сохранялся.