/************************************************************* Jogo da Velha — OLED SSD1306 128x64 + Keypad 4x4 + Buzzer (passivo) Versão: alternância e reinício automático - Sons estilo 8-bit (melodias) - Humano: X (sempre) - IA: O (sempre) — níveis IA1 (fraca), IA2 (média), IA3 (forte) - Tecla A: PvP -> IA1 -> IA2 -> IA3 -> PvP - Tecla B: muda nível IA (quando em IA) - Tecla *: reinicia partida (mantém placar, alterna iniciador) - Tecla #: zera placar e reinicia (iniciador volta para X) - Vez e Modo no header; placar X= / O= - POP animation, risco/pisca no trio vencedor, reinício automático Autor: Angelo Luis Ferreira com auxílio do ChatGPT 05/12/2025 *************************************************************/ #include #include #include #include #define OLED_WIDTH 128 #define OLED_HEIGHT 64 #define OLED_RESET -1 Adafruit_SSD1306 oled(OLED_WIDTH, OLED_HEIGHT, &Wire, OLED_RESET); // Keypad const byte ROWS = 4, COLS = 4; char keys[ROWS][COLS] = { {'1','2','3','A'}, {'4','5','6','B'}, {'7','8','9','C'}, {'*','0','#','D'} }; byte rowPins[ROWS] = {9,8,7,6}; byte colPins[COLS] = {5,4,3,2}; Keypad keypad = Keypad(makeKeymap(keys), rowPins, colPins, ROWS, COLS); // Buzzer (use buzzer passivo) #define BUZZER 10 // Game state char board[9]; // ' ' | 'X' | 'O' bool isPvAI = true; // current mode; if false => PvP bool humanTurn = true; // true = X (human) turn bool nextStartsX = true; // alternator for who starts next match int scoreX = 0; int scoreO = 0; // IA difficulty: 1 = IA1 (weak), 2 = IA2 (medium), 3 = IA3 (strong) int aiLevel = 3; // Layout const int headerHeight = 16; const int boardTop = headerHeight; int cellW, cellH; // redraw control bool needRedraw = true; bool gameActive = false; // NEW: only true when board is drawn and ready to accept moves/IA // Winning combos const int WINS[8][3] = { {0,1,2},{3,4,5},{6,7,8}, {0,3,6},{1,4,7},{2,5,8}, {0,4,8},{2,4,6} }; // Prototypes void initBoard(); void drawScreen(); void drawHeader(); void drawBoardLines(); void drawPieces(); void popupAnimate(int pos, char p); void popupAnimateCore(int pos, char p); int evaluateBoard(const char b[9]); bool movesLeft(const char b[9]); int minimax(char b[9], bool isMax, int depth); int bestMoveMinimax(); int aiMove(); int aiMoveMedium(); int aiMoveWeak(); void victoryBlink(int winIndex, char winnerChar); void drawWinningLine(int winIndex); void playStartSound(); void playToggleSound(); void playSoundX(); void playSoundO(); void playErrorSound(); void playVictoryMelody(); void playDrawMelody(); void toneDelay(int freq, int dur); // ------------------------------------------------------------------ void setup() { pinMode(BUZZER, OUTPUT); randomSeed(analogRead(A0)); if(!oled.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { while (1); } oled.clearDisplay(); oled.display(); int availH = OLED_HEIGHT - headerHeight; // 48 cellW = OLED_WIDTH / 3; // ~42 cellH = availH / 3; // 16 // initial starter = X nextStartsX = true; humanTurn = nextStartsX; initBoard(); // initial splash oled.setTextSize(1); oled.setTextColor(SSD1306_WHITE); oled.setCursor(10,20); oled.print("Jogo da Velha"); oled.setCursor(10,34); oled.print("Pressione * para iniciar"); oled.display(); playStartSound(); } void loop() { // If PvAI and it's AI turn, let AI play immediately (no waiting for key) // IMPORTANT: only when gameActive == true (board drawn and ready) if (isPvAI && !humanTurn && gameActive) { delay(120); // small "thinking" int mv = aiMove(); if (mv >= 0) { board[mv] = 'O'; popupAnimate(mv, 'O'); playSoundO(); humanTurn = true; needRedraw = true; // after a move, leave gameActive true until redraw sets it again } } char k = keypad.getKey(); if (k) { if (k == 'A') { // Cycle: PvP -> IA1 -> IA2 -> IA3 -> PvP if (!isPvAI) { isPvAI = true; aiLevel = 1; } else { aiLevel++; if (aiLevel > 3) { isPvAI = false; aiLevel = 3; // keep last for re-entry } } playToggleSound(); needRedraw = true; } else if (k == 'B') { // Only change aiLevel if currently IA mode if (isPvAI) { aiLevel++; if (aiLevel > 3) aiLevel = 1; playToggleSound(); needRedraw = true; } else { playErrorSound(); } } else if (k == '*') { // alterna quem inicia and restart (keep scores) nextStartsX = !nextStartsX; humanTurn = nextStartsX; // set who starts BEFORE initBoard initBoard(); playStartSound(); } else if (k == '#') { // zero scores and restart (starter back to X) scoreX = 0; scoreO = 0; nextStartsX = true; humanTurn = nextStartsX; // ensure X starts initBoard(); playStartSound(); needRedraw = true; } else if (k >= '1' && k <= '9') { int pos = k - '1'; if (pos < 0 || pos > 8) return; if (!gameActive) { // ignore key presses while transitioning (optional feedback) } else { if (humanTurn) { if (board[pos] == ' ') { board[pos] = 'X'; popupAnimate(pos, 'X'); playSoundX(); humanTurn = false; needRedraw = true; } else { playErrorSound(); } } else { // if PvP and it's O human's turn, accept move if (!isPvAI) { if (board[pos] == ' ') { board[pos] = 'O'; popupAnimate(pos, 'O'); playSoundO(); humanTurn = true; needRedraw = true; } else { playErrorSound(); } } } } } } if (needRedraw) { // Draw and mark gameActive true AFTER drawing so AI can act properly drawScreen(); needRedraw = false; gameActive = true; // after drawing, check winner/draw instantly so UI is coherent int winIndex = -1; for (int i=0;i<8;i++) { int a = WINS[i][0], b = WINS[i][1], c = WINS[i][2]; if (board[a] != ' ' && board[a] == board[b] && board[b] == board[c]) { winIndex = i; break; } } if (winIndex != -1) { // freeze input while showing sequence gameActive = false; char winnerChar = board[WINS[winIndex][0]]; // 'X' or 'O' if (winnerChar == 'X') scoreX++; else scoreO++; playVictoryMelody(); victoryBlink(winIndex, winnerChar); // prepare next match: toggle starter, set humanTurn accordingly, then initBoard() nextStartsX = !nextStartsX; humanTurn = nextStartsX; // set who will start next initBoard(); // clears board and marks needRedraw true // we intentionally keep needRedraw true to trigger drawScreen in next loop return; } // check draw bool full = true; for (int i=0;i<9;i++) if (board[i] == ' ') { full = false; break; } if (full) { gameActive = false; playDrawMelody(); // blink whole board to show draw for (int t=0;t<3;t++) { oled.clearDisplay(); oled.display(); delay(160); drawScreen(); delay(160); } // next match: toggle starter, set humanTurn, initBoard nextStartsX = !nextStartsX; humanTurn = nextStartsX; initBoard(); return; } } delay(8); } // -------------------------- AUX -------------------------------- void initBoard() { for (int i=0;i<9;i++) board[i] = ' '; // IMPORTANT: do NOT overwrite humanTurn here (set by caller) gameActive = false; // not active until board is drawn needRedraw = true; // request redraw; after drawScreen we'll set gameActive=true } void drawScreen() { oled.clearDisplay(); drawHeader(); drawBoardLines(); drawPieces(); oled.display(); } void drawHeader() { // Line 0: X=score (left) and O=score (right) // Line 1: Vez:X/O Modo: PvP / IA1 / IA2 / IA3 oled.setTextSize(1); oled.setTextColor(SSD1306_WHITE); // X score left oled.setCursor(0,0); oled.print("X="); oled.print(scoreX); // O score right oled.setCursor(96,0); oled.print("O="); oled.print(scoreO); // Second line: Vez and Modo oled.setCursor(0,8); oled.print("Vez:"); oled.print(humanTurn ? "X" : "O"); // Modo to the right oled.setCursor(68,8); oled.print("Modo:"); oled.print(" "); if (!isPvAI) { oled.print("PvP"); } else { if (aiLevel == 1) oled.print("IA1"); else if (aiLevel == 2) oled.print("IA2"); else oled.print("IA3"); } } void drawBoardLines() { int y1 = boardTop + cellH; int y2 = boardTop + 2*cellH; oled.drawLine(0, y1, OLED_WIDTH, y1, SSD1306_WHITE); oled.drawLine(0, y2, OLED_WIDTH, y2, SSD1306_WHITE); int x1 = cellW; int x2 = 2*cellW; oled.drawLine(x1, boardTop, x1, OLED_HEIGHT, SSD1306_WHITE); oled.drawLine(x2, boardTop, x2, OLED_HEIGHT, SSD1306_WHITE); } void drawPieces() { for (int i=0;i<9;i++) { int row = i / 3; int col = i % 3; int cx = col * cellW + cellW/2; int cy = boardTop + row * cellH + cellH/2; int r = min(cellW, cellH) / 3; if (board[i] == 'X') { oled.drawLine(cx - r, cy - r, cx + r, cy + r, SSD1306_WHITE); oled.drawLine(cx + r, cy - r, cx - r, cy + r, SSD1306_WHITE); } else if (board[i] == 'O') { oled.drawCircle(cx, cy, r, SSD1306_WHITE); } } } // ---------------------- POP animation ------------------------ void popupAnimate(int pos, char p) { popupAnimateCore(pos, p); needRedraw = true; } void popupAnimateCore(int pos, char p) { int row = pos / 3; int col = pos % 3; int cx = col * cellW + cellW/2; int cy = boardTop + row * cellH + cellH/2; int rmax = min(cellW, cellH) / 3; for (int r = 2; r <= rmax; r += 2) { oled.clearDisplay(); drawHeader(); drawBoardLines(); // draw existing pieces except the animating one for (int i = 0; i < 9; i++) { if (i == pos) continue; int rowi = i / 3, coli = i % 3; int cxi = coli * cellW + cellW/2; int cyi = boardTop + rowi * cellH + cellH/2; int rr = min(cellW, cellH) / 3; if (board[i] == 'X') { oled.drawLine(cxi - rr, cyi - rr, cxi + rr, cyi + rr, SSD1306_WHITE); oled.drawLine(cxi + rr, cyi - rr, cxi - rr, cyi + rr, SSD1306_WHITE); } else if (board[i] == 'O') { oled.drawCircle(cxi, cyi, rr, SSD1306_WHITE); } } // draw growing piece if (p == 'X') { oled.drawLine(cx - r, cy - r, cx + r, cy + r, SSD1306_WHITE); oled.drawLine(cx + r, cy - r, cx - r, cy + r, SSD1306_WHITE); } else { oled.drawCircle(cx, cy, r, SSD1306_WHITE); } oled.display(); delay(36); } } // ---------------------- AI (3 níveis) ------------------------ int evaluateBoard(const char b[9]) { for (int i=0;i<8;i++) { int a=WINS[i][0], bb=WINS[i][1], c=WINS[i][2]; if (b[a] != ' ' && b[a] == b[bb] && b[bb] == b[c]) { if (b[a] == 'O') return +10; if (b[a] == 'X') return -10; } } return 0; } bool movesLeft(const char b[9]) { for (int i=0;i<9;i++) if (b[i] == ' ') return true; return false; } int minimax(char b[9], bool isMax, int depth, int alpha, int beta) { int score = evaluateBoard(b); if (score == 10) return score - depth; if (score == -10) return score + depth; if (!movesLeft(b)) return 0; if (isMax) { // O (AI) int best = -1000; for (int i = 0; i < 9; i++) { if (b[i] == ' ') { b[i] = 'O'; int val = minimax(b, false, depth + 1, alpha, beta); b[i] = ' '; best = max(best, val); alpha = max(alpha, best); if (beta <= alpha) break; // poda } } return best; } else { // X (humano) int best = 1000; for (int i = 0; i < 9; i++) { if (b[i] == ' ') { b[i] = 'X'; int val = minimax(b, true, depth + 1, alpha, beta); b[i] = ' '; best = min(best, val); beta = min(beta, best); if (beta <= alpha) break; // poda } } return best; } } int bestMoveMinimax() { int bestVal = -1000; int bestMove = -1; for (int i = 0; i < 9; i++) { if (board[i] == ' ') { board[i] = 'O'; int moveVal = minimax(board, false, 0, -1000, 1000); // agora com alpha/beta board[i] = ' '; if (moveVal > bestVal) { bestVal = moveVal; bestMove = i; } } } return bestMove; } // IA média: heuristic (win/block/center/opposite corner/corner/side) int aiMoveMedium() { // 1 - win immediate for (int i=0;i<9;i++) if (board[i]==' ') { board[i] = 'O'; for (int w=0; w<8; w++) { int a=WINS[w][0], b=WINS[w][1], c=WINS[w][2]; if (board[a] != ' ' && board[a]==board[b] && board[b]==board[c]) { board[i] = ' '; return i; } } board[i] = ' '; } // 2 - block opponent for (int i=0;i<9;i++) if (board[i]==' ') { board[i] = 'X'; for (int w=0; w<8; w++) { int a=WINS[w][0], b=WINS[w][1], c=WINS[w][2]; if (board[a] != ' ' && board[a]==board[b] && board[b]==board[c]) { board[i] = ' '; return i; } } board[i] = ' '; } // 3 - center if (board[4] == ' ') return 4; // 4 - opposite corner int corners[4] = {0,2,6,8}; for (int i=0;i<4;i++) { int c = corners[i]; int opp = 8 - c; if (board[c]=='X' && board[opp]==' ') return opp; } // 5 - empty corner for (int i=0;i<4;i++) if (board[corners[i]]==' ') return corners[i]; // 6 - side int sides[4] = {1,3,5,7}; for (int i=0;i<4;i++) if (board[sides[i]]==' ') return sides[i]; // fallback for (int i=0;i<9;i++) if (board[i]==' ') return i; return -1; } // IA fraca: random available int aiMoveWeak() { int empties[9], n=0; for (int i=0;i<9;i++) if (board[i]==' ') empties[n++]=i; if (n==0) return -1; return empties[random(0,n)]; } int aiMove() { if (aiLevel == 1) return aiMoveWeak(); if (aiLevel == 2) return aiMoveMedium(); return bestMoveMinimax(); // strong } // ------------------ victory blink / line -------------------- void victoryBlink(int winIndex, char winnerChar) { // blink 3 times: show thick line overlay then clear (pieces remain) for (int t = 0; t < 3; t++) { // show line overlay drawScreen(); drawWinningLine(winIndex); oled.display(); // short tone bursts while showing if (winnerChar == 'X') { toneDelay(1200 + t*80, 110); } else { toneDelay(800 + t*60, 110); } delay(160); // clear overlay (just redraw) drawScreen(); oled.display(); delay(120); } } void drawWinningLine(int winIndex) { int a = WINS[winIndex][0]; int c = WINS[winIndex][2]; int ar = a / 3, ac = a % 3; int cr = c / 3, cc = c % 3; int ax = ac * cellW + cellW/2; int ay = boardTop + ar * cellH + cellH/2; int cx = cc * cellW + cellW/2; int cy = boardTop + cr * cellH + cellH/2; // thicker line for (int w = -2; w <= 2; w++) { oled.drawLine(ax + w, ay + w, cx + w, cy + w, SSD1306_WHITE); } } // ------------------ SOUND (8-bit style) -------------------- // Small helper: play tone and block for dur ms (for chiptune feel) void toneDelay(int freq, int dur) { tone(BUZZER, freq); delay(dur); noTone(BUZZER); delay(8); // tiny gap for chiptune articulation } // Start game melody (short fanfare) void playStartSound() { toneDelay(520, 80); toneDelay(780, 80); toneDelay(1040, 120); } // Toggle / change mode sound (short chirp) void playToggleSound() { toneDelay(480, 40); toneDelay(720, 40); } // Sound for player X (arpeggio, 8-bit) void playSoundX() { toneDelay(900, 60); toneDelay(1200, 50); toneDelay(1500, 70); } // Sound for player O (IA) (lower arpeggio) void playSoundO() { toneDelay(650, 70); toneDelay(850, 60); toneDelay(1050, 70); } // Error / occupied position (vibrato-ish) void playErrorSound() { for (int i=0;i<3;i++){ tone(BUZZER, 220 + i*20); delay(60); } noTone(BUZZER); delay(30); } // Victory melody (more elaborate ~700ms) void playVictoryMelody() { // a short 8-bit celebratory phrase toneDelay(880, 80); toneDelay(1320, 80); toneDelay(1760, 100); toneDelay(1320, 80); toneDelay(1040, 80); toneDelay(880, 120); } // Draw/Empate melody (short) void playDrawMelody() { toneDelay(600, 80); toneDelay(500, 80); toneDelay(400, 120); }