/*
* 조이스틱 틱택토 게임 - 통합 코드 (Non-blocking)
* - 하드웨어: Arduino Nano, OLED 디스플레이(SSD1306), 조이스틱, 부저
* - 연결:
* - OLED: A4(SDA), A5(SCL)
* - 조이스틱: A0(X축), A1(Y축), D2(버튼)
* - 부저: D3
*/
#include <U8glib.h>
#include <Wire.h>
// OLED 디스플레이 초기화
U8GLIB_SSD1306_128X64 u8g(U8G_I2C_OPT_NONE);
// 핀 정의
const int JOYSTICK_X = A0; // 조이스틱 X축
const int JOYSTICK_Y = A1; // 조이스틱 Y축
const int JOYSTICK_BTN = 3; // 조이스틱 버튼
const int BUZZER_PIN = 4; // 부저 핀
// 게임 효과음을 위한 음계 정의
#define NOTE_C4 262 // 도
#define NOTE_D4 294 // 레
#define NOTE_E4 330 // 미
#define NOTE_F4 349 // 파
#define NOTE_G4 392 // 솔
#define NOTE_A4 440 // 라
#define NOTE_B4 494 // 시
#define NOTE_C5 523 // 높은 도
// 게임 상태 정의
enum GameState
{
STATE_INTRO, // 시작 화면
STATE_PLAYER_TURN, // 플레이어 턴
STATE_CPU_TURN, // CPU 턴
STATE_GAME_OVER // 게임 종료
};
// 게임 변수
GameState currentState = STATE_INTRO; // 현재 게임 상태
boolean playerIsX = true; // 플레이어가 X를 사용하는지 여부
int cursorRow = 1; // 커서 행 위치 (0-2)
int cursorCol = 1; // 커서 열 위치 (0-2)
int board[3][3] = {0}; // 게임판 상태 (0=빈칸, 1=X, 2=O)
boolean gameEnded = false; // 게임 종료 여부
int winner = 0; // 승자 (0=없음, 1=X, 2=O)
unsigned long lastDisplayUpdateTime = 0; // 마지막 화면 업데이트 시간
// 타이밍 변수
unsigned long lastMoveTime = 0; // 마지막 커서 이동 시간
unsigned long lastButtonTime = 0; // 마지막 버튼 누름 시간
unsigned long gameStartTime = 0; // 게임 시작 시간
unsigned long lastCpuMoveTime = 0; // 마지막 CPU 이동 시간
unsigned long soundStartTime = 0; // 효과음 시작 시간
unsigned long stateChangeTime = 0; // 상태 변경 시간
// 소리 관련 변수
boolean isSoundPlaying = false; // 소리 재생 중 여부
int currentSoundStep = 0; // 현재 소리 재생 단계
int soundToPlay = 0; // 0=없음, 1=시작, 2=버튼, 3=X, 4=O, 5=승리, 6=무승부, 7=에러
// 상수 정의
const int DEBOUNCE_DELAY = 200; // 디바운싱 딜레이 (ms)
const int DISPLAY_UPDATE_INTERVAL = 100; // 디스플레이 업데이트 주기 (ms)
const int CPU_MOVE_DELAY = 1000; // CPU 이동 대기 시간 (ms)
// 버튼 상태 변수
boolean buttonState = HIGH; // 현재 버튼 상태
boolean lastButtonState = HIGH; // 이전 버튼 상태
void setup()
{
Serial.begin(9600);
Serial.println("조이스틱 틱택토 게임 시작");
// 핀 설정
pinMode(JOYSTICK_X, INPUT);
pinMode(JOYSTICK_Y, INPUT);
pinMode(JOYSTICK_BTN, INPUT_PULLUP);
pinMode(BUZZER_PIN, OUTPUT);
// OLED 초기화
u8g.setFont(u8g_font_6x10);
u8g.setColorIndex(1);
// 게임 시작 시간 기록
gameStartTime = millis();
stateChangeTime = gameStartTime;
// 시작 효과음 재생 시작
startSound(1); // 시작 소리 코드
// 게임판 초기화
resetGame();
}
void loop()
{
unsigned long currentMillis = millis();
// 효과음 처리 (Non-blocking)
handleSoundEffect(currentMillis);
// 현재 게임 상태에 따른 처리
switch (currentState)
{
case STATE_INTRO:
handleIntroState(currentMillis);
break;
case STATE_PLAYER_TURN:
handleJoystickInput(currentMillis);
break;
case STATE_CPU_TURN:
handleCpuTurn(currentMillis);
break;
case STATE_GAME_OVER:
handleGameOverState(currentMillis);
break;
}
// 화면 업데이트 (업데이트 주기에 따라)
if (currentMillis - lastDisplayUpdateTime >= DISPLAY_UPDATE_INTERVAL)
{
updateDisplay();
lastDisplayUpdateTime = currentMillis;
}
}
// Non-blocking 소리 재생 처리
void handleSoundEffect(unsigned long currentMillis)
{
if (!isSoundPlaying || soundToPlay == 0)
return;
switch (soundToPlay)
{
case 1: // 시작 소리
playStartSoundNB(currentMillis);
break;
case 2: // 버튼 클릭 소리
playButtonSoundNB(currentMillis);
break;
case 3: // X 표시 소리
playXSoundNB(currentMillis);
break;
case 4: // O 표시 소리
playOSoundNB(currentMillis);
break;
case 5: // 승리 소리
playWinSoundNB(currentMillis);
break;
case 6: // 무승부 소리
playDrawSoundNB(currentMillis);
break;
case 7: // 에러 소리
playErrorSoundNB(currentMillis);
break;
}
}
// 시작 화면 상태 처리
void handleIntroState(unsigned long currentMillis)
{
// 자동 시작 비활성화 (버튼 입력만으로 게임 시작)
// if (currentMillis - stateChangeTime >= 2000 && !isSoundPlaying)
// {
// currentState = STATE_PLAYER_TURN;
// stateChangeTime = currentMillis;
// }
// 버튼을 눌러야만 게임 시작
boolean reading = digitalRead(JOYSTICK_BTN);
if (reading != lastButtonState)
{
lastButtonTime = currentMillis;
}
if ((currentMillis - lastButtonTime) > DEBOUNCE_DELAY)
{
if (reading != buttonState)
{
buttonState = reading;
if (buttonState == LOW && !isSoundPlaying)
{
currentState = STATE_PLAYER_TURN;
stateChangeTime = currentMillis;
startSound(2); // 버튼 소리 재생
}
}
}
lastButtonState = reading;
}
// 조이스틱 입력 처리 (플레이어 턴)
void handleJoystickInput(unsigned long currentMillis)
{
// 조이스틱 값 읽기
int xValue = analogRead(JOYSTICK_X);
int yValue = analogRead(JOYSTICK_Y);
// 커서 이동 처리 (디바운싱 적용)
if (currentMillis - lastMoveTime > DEBOUNCE_DELAY)
{
boolean moved = false;
// X축 (좌-우)
if (xValue < 400)
{ // 왼쪽
if (cursorCol > 0)
{
cursorCol--;
moved = true;
}
}
else if (xValue > 600)
{ // 오른쪽
if (cursorCol < 2)
{
cursorCol++;
moved = true;
}
}
// Y축 (위-아래)
if (yValue < 400)
{ // 위쪽
if (cursorRow > 0)
{
cursorRow--;
moved = true;
}
}
else if (yValue > 600)
{ // 아래쪽
if (cursorRow < 2)
{
cursorRow++;
moved = true;
}
}
// 커서가 움직였으면 타이머 갱신
if (moved)
{
lastMoveTime = currentMillis;
startSound(2); // 버튼 소리 재생
}
}
// 버튼 입력 처리
boolean reading = digitalRead(JOYSTICK_BTN);
if (reading != lastButtonState)
{
lastButtonTime = currentMillis;
}
if ((currentMillis - lastButtonTime) > DEBOUNCE_DELAY)
{
if (reading != buttonState)
{
buttonState = reading;
// 버튼이 눌러졌을 때 (LOW)
if (buttonState == LOW)
{
// 빈 칸이면 말 놓기
if (board[cursorRow][cursorCol] == 0)
{
board[cursorRow][cursorCol] = playerIsX ? 1 : 2; // X 또는 O 놓기
startSound(playerIsX ? 3 : 4); // X 또는 O 소리 재생
// 승리 또는 무승부 체크
checkGameResult();
if (!gameEnded)
{
// CPU 턴으로 전환
currentState = STATE_CPU_TURN;
lastCpuMoveTime = currentMillis;
}
else
{
// 게임 종료
currentState = STATE_GAME_OVER;
stateChangeTime = currentMillis;
// 승리 또는 무승부 소리 재생
if (winner > 0)
{
startSound(5); // 승리 소리
}
else
{
startSound(6); // 무승부 소리
}
}
}
else
{
// 이미 말이 있는 위치면 에러 소리
startSound(7); // 에러 소리
}
}
}
}
lastButtonState = reading;
}
// CPU 턴 처리
void handleCpuTurn(unsigned long currentMillis)
{
// CPU가 생각하는 시간 (1초) 후 이동
if (currentMillis - lastCpuMoveTime >= CPU_MOVE_DELAY && !isSoundPlaying)
{
// CPU 이동 로직 (간단한 랜덤 이동)
makeCpuMove();
// 승리 또는 무승부 체크
checkGameResult();
if (!gameEnded)
{
// 플레이어 턴으로 전환
currentState = STATE_PLAYER_TURN;
}
else
{
// 게임 종료
currentState = STATE_GAME_OVER;
stateChangeTime = currentMillis;
// 승리 또는 무승부 소리 재생
if (winner > 0)
{
startSound(5); // 승리 소리
}
else
{
startSound(6); // 무승부 소리
}
}
}
}
// 게임 종료 상태 처리
void handleGameOverState(unsigned long currentMillis)
{
// 결과를 표시한 후 최소 2초 이상 경과해야 함
boolean resultTimeElapsed = (currentMillis - stateChangeTime >= 2000);
// 효과음이 끝나고 2초 이상 지나거나 버튼을 누르면 새 게임 시작
// 단, 효과음 재생 중에는 건너뛸 수 없음
if ((resultTimeElapsed && !isSoundPlaying) ||
(buttonState == LOW && (currentMillis - lastButtonTime) > DEBOUNCE_DELAY && !isSoundPlaying && resultTimeElapsed))
{
resetGame();
currentState = STATE_PLAYER_TURN;
stateChangeTime = currentMillis;
startSound(1); // 시작 소리 재생
}
// 버튼 상태 체크
boolean reading = digitalRead(JOYSTICK_BTN);
if (reading != lastButtonState)
{
lastButtonTime = currentMillis;
}
if ((currentMillis - lastButtonTime) > DEBOUNCE_DELAY)
{
if (reading != buttonState)
{
buttonState = reading;
}
}
lastButtonState = reading;
}
// CPU 이동 로직 (간단한 랜덤 이동)
void makeCpuMove()
{
int cpuValue = playerIsX ? 2 : 1; // CPU가 O 또는 X 사용
// 1. 승리할 수 있는 자리 찾기
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 3; j++)
{
if (board[i][j] == 0)
{
// 임시로 말을 놓아 승리 가능성 체크
board[i][j] = cpuValue;
if (checkWin(cpuValue))
{
startSound(playerIsX ? 4 : 3); // O 또는 X 소리 재생
return; // 승리할 수 있는 자리 찾음
}
board[i][j] = 0; // 원상복구
}
}
}
// 2. 상대방이 승리할 수 있는 자리 막기
int playerValue = playerIsX ? 1 : 2;
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 3; j++)
{
if (board[i][j] == 0)
{
// 임시로 상대방 말을 놓아 승리 가능성 체크
board[i][j] = playerValue;
if (checkWin(playerValue))
{
board[i][j] = cpuValue; // 이 자리에 CPU 말 놓기
startSound(playerIsX ? 4 : 3); // O 또는 X 소리 재생
return; // 상대방 승리 방지
}
board[i][j] = 0; // 원상복구
}
}
}
// 3. 중앙이 비어있으면 중앙에 놓기
if (board[1][1] == 0)
{
board[1][1] = cpuValue;
startSound(playerIsX ? 4 : 3); // O 또는 X 소리 재생
return;
}
// 4. 랜덤으로 빈 자리 찾기
int emptySpots = 0;
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 3; j++)
{
if (board[i][j] == 0)
emptySpots++;
}
}
if (emptySpots > 0)
{
int randomSpot = random(emptySpots);
emptySpots = 0;
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 3; j++)
{
if (board[i][j] == 0)
{
if (emptySpots == randomSpot)
{
board[i][j] = cpuValue;
startSound(playerIsX ? 4 : 3); // O 또는 X 소리 재생
return;
}
emptySpots++;
}
}
}
}
}
// 게임 결과 체크 (승리 또는 무승부)
void checkGameResult()
{
// X 승리 체크
if (checkWin(1))
{
winner = 1;
gameEnded = true;
return;
}
// O 승리 체크
if (checkWin(2))
{
winner = 2;
gameEnded = true;
return;
}
// 무승부 체크 (모든 칸이 채워진 경우)
boolean isFull = true;
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 3; j++)
{
if (board[i][j] == 0)
{
isFull = false;
break;
}
}
if (!isFull)
break;
}
if (isFull)
{
winner = 0; // 무승부
gameEnded = true;
}
}
// 승리 체크 (같은 값이 3개 연속되는지)
boolean checkWin(int player)
{
// 가로줄 체크
for (int i = 0; i < 3; i++)
{
if (board[i][0] == player && board[i][1] == player && board[i][2] == player)
{
return true;
}
}
// 세로줄 체크
for (int i = 0; i < 3; i++)
{
if (board[0][i] == player && board[1][i] == player && board[2][i] == player)
{
return true;
}
}
// 대각선 체크 (왼쪽 위 - 오른쪽 아래)
if (board[0][0] == player && board[1][1] == player && board[2][2] == player)
{
return true;
}
// 대각선 체크 (오른쪽 위 - 왼쪽 아래)
if (board[0][2] == player && board[1][1] == player && board[2][0] == player)
{
return true;
}
return false;
}
// 게임 리셋
void resetGame()
{
// 게임판 초기화
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 3; j++)
{
board[i][j] = 0;
}
}
// 변수 초기화
cursorRow = 1;
cursorCol = 1;
gameEnded = false;
winner = 0;
// 시작하는 사람 랜덤 결정 (여기서는 항상 사용자가 먼저 시작하도록 설정)
// playerIsX = random(2) == 0;
}
// 화면 업데이트
void updateDisplay()
{
u8g.firstPage();
do
{
// 게임 상태에 따라 다른 화면 그리기
switch (currentState)
{
case STATE_INTRO:
drawIntroScreen();
break;
case STATE_PLAYER_TURN:
case STATE_CPU_TURN:
drawGameScreen();
break;
case STATE_GAME_OVER:
drawGameOverScreen();
break;
}
} while (u8g.nextPage());
}
// 시작 화면 그리기
void drawIntroScreen()
{
u8g.setFont(u8g_font_6x10);
u8g.drawStr(28, 20, "TIC TAC TOE");
u8g.drawStr(15, 40, "PRESS BUTTON TO");
u8g.drawStr(40, 50, "START");
}
// 게임 화면 그리기
void drawGameScreen()
{
// 게임판 그리기
drawBoard();
// 말 그리기 (X와 O)
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 3; j++)
{
if (board[i][j] == 1)
{ // X
drawX(i, j);
}
else if (board[i][j] == 2)
{ // O
drawO(i, j);
}
}
}
// 현재 플레이어 표시
u8g.drawStr(5, 10, currentState == STATE_PLAYER_TURN ? "YOUR TURN" : "CPU TURN");
// 플레이어가 X인지 O인지 표시
u8g.drawStr(80, 10, playerIsX ? "YOU: X" : "YOU: O");
// 현재 선택된 셀 표시 (플레이어 턴일 때만)
if (currentState == STATE_PLAYER_TURN)
{
highlightCell(cursorRow, cursorCol);
}
}
// 게임 종료 화면 그리기
void drawGameOverScreen()
{
// 게임판 그리기
drawBoard();
// 말 그리기 (X와 O)
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 3; j++)
{
if (board[i][j] == 1)
{ // X
drawX(i, j);
}
else if (board[i][j] == 2)
{ // O
drawO(i, j);
}
}
}
// 결과 표시
u8g.drawStr(30, 10, "GAME OVER");
if (winner == 0)
{
u8g.drawStr(45, 63, "DRAW!");
}
else if ((winner == 1 && playerIsX) || (winner == 2 && !playerIsX))
{
u8g.drawStr(37, 63, "YOU WIN!");
}
else
{
u8g.drawStr(37, 63, "CPU WIN!");
}
}
// 틱택토 게임판 그리기
void drawBoard()
{
u8g.drawLine(42, 20, 42, 63); // 세로선 1
u8g.drawLine(84, 20, 84, 63); // 세로선 2
u8g.drawLine(0, 34, 128, 34); // 가로선 1
u8g.drawLine(0, 49, 128, 49); // 가로선 2
}
// X 표시 그리기 (row, col: 0-2)
void drawX(int row, int col)
{
int x = col * 42 + 10;
int y = row * 15 + 25;
u8g.drawLine(x, y, x + 20, y + 8);
u8g.drawLine(x, y + 8, x + 20, y);
}
// O 표시 그리기 (row, col: 0-2)
void drawO(int row, int col)
{
int x = col * 42 + 21;
int y = row * 15 + 29;
u8g.drawCircle(x, y, 6);
}
// 선택된 셀 하이라이트 표시
void highlightCell(int row, int col)
{
int x = col * 42 + 1;
int y = row * 15 + 21;
u8g.setColorIndex(2); // XOR 모드
u8g.drawBox(x, y, 40, 12);
u8g.setColorIndex(1); // 일반 모드로 복원
}
// 효과음 시작
void startSound(int soundCode)
{
soundToPlay = soundCode;
isSoundPlaying = true;
currentSoundStep = 0;
soundStartTime = millis();
}
//----- Non-blocking 효과음 함수들 -----
// 시작 신호음 (Non-blocking)
void playStartSoundNB(unsigned long currentMillis)
{
unsigned long elapsed = currentMillis - soundStartTime;
if (currentSoundStep == 0 && elapsed >= 0)
{
tone(BUZZER_PIN, NOTE_C4, 100);
currentSoundStep = 1;
soundStartTime = currentMillis;
}
else if (currentSoundStep == 1 && elapsed >= 150)
{
tone(BUZZER_PIN, NOTE_E4, 100);
currentSoundStep = 2;
soundStartTime = currentMillis;
}
else if (currentSoundStep == 2 && elapsed >= 150)
{
tone(BUZZER_PIN, NOTE_G4, 100);
currentSoundStep = 3;
soundStartTime = currentMillis;
}
else if (currentSoundStep == 3 && elapsed >= 150)
{
tone(BUZZER_PIN, NOTE_C5, 300);
currentSoundStep = 4;
soundStartTime = currentMillis;
}
else if (currentSoundStep == 4 && elapsed >= 350)
{
// 효과음 종료
noTone(BUZZER_PIN);
isSoundPlaying = false;
soundToPlay = 0;
}
}
// 버튼 클릭 효과음 (Non-blocking)
void playButtonSoundNB(unsigned long currentMillis)
{
unsigned long elapsed = currentMillis - soundStartTime;
if (currentSoundStep == 0 && elapsed >= 0)
{
tone(BUZZER_PIN, NOTE_A4, 50);
currentSoundStep = 1;
soundStartTime = currentMillis;
}
else if (currentSoundStep == 1 && elapsed >= 100)
{
// 효과음 종료
noTone(BUZZER_PIN);
isSoundPlaying = false;
soundToPlay = 0;
}
}
// X 표시 효과음 (Non-blocking)
void playXSoundNB(unsigned long currentMillis)
{
unsigned long elapsed = currentMillis - soundStartTime;
if (currentSoundStep == 0 && elapsed >= 0)
{
tone(BUZZER_PIN, NOTE_G4, 100);
currentSoundStep = 1;
soundStartTime = currentMillis;
}
else if (currentSoundStep == 1 && elapsed >= 150)
{
// 효과음 종료
noTone(BUZZER_PIN);
isSoundPlaying = false;
soundToPlay = 0;
}
}
// O 표시 효과음 (Non-blocking)
void playOSoundNB(unsigned long currentMillis)
{
unsigned long elapsed = currentMillis - soundStartTime;
if (currentSoundStep == 0 && elapsed >= 0)
{
tone(BUZZER_PIN, NOTE_E4, 100);
currentSoundStep = 1;
soundStartTime = currentMillis;
}
else if (currentSoundStep == 1 && elapsed >= 150)
{
// 효과음 종료
noTone(BUZZER_PIN);
isSoundPlaying = false;
soundToPlay = 0;
}
}
// 승리 효과음 (Non-blocking)
void playWinSoundNB(unsigned long currentMillis)
{
unsigned long elapsed = currentMillis - soundStartTime;
if (currentSoundStep == 0 && elapsed >= 0)
{
tone(BUZZER_PIN, NOTE_C4, 100);
currentSoundStep = 1;
soundStartTime = currentMillis;
}
else if (currentSoundStep == 1 && elapsed >= 150)
{
tone(BUZZER_PIN, NOTE_E4, 100);
currentSoundStep = 2;
soundStartTime = currentMillis;
}
else if (currentSoundStep == 2 && elapsed >= 150)
{
tone(BUZZER_PIN, NOTE_G4, 100);
currentSoundStep = 3;
soundStartTime = currentMillis;
}
else if (currentSoundStep == 3 && elapsed >= 150)
{
tone(BUZZER_PIN, NOTE_C5, 300);
currentSoundStep = 4;
soundStartTime = currentMillis;
}
else if (currentSoundStep == 4 && elapsed >= 350)
{
// 효과음 종료
noTone(BUZZER_PIN);
isSoundPlaying = false;
soundToPlay = 0;
}
}
// 무승부 효과음 (Non-blocking)
void playDrawSoundNB(unsigned long currentMillis)
{
unsigned long elapsed = currentMillis - soundStartTime;
if (currentSoundStep == 0 && elapsed >= 0)
{
tone(BUZZER_PIN, NOTE_C4, 100);
currentSoundStep = 1;
soundStartTime = currentMillis;
}
else if (currentSoundStep == 1 && elapsed >= 150)
{
tone(BUZZER_PIN, NOTE_C4, 100);
currentSoundStep = 2;
soundStartTime = currentMillis;
}
else if (currentSoundStep == 2 && elapsed >= 150)
{
tone(BUZZER_PIN, NOTE_C4, 300);
currentSoundStep = 3;
soundStartTime = currentMillis;
}
else if (currentSoundStep == 3 && elapsed >= 350)
{
// 효과음 종료
noTone(BUZZER_PIN);
isSoundPlaying = false;
soundToPlay = 0;
}
}
// 에러/잘못된 입력 효과음 (Non-blocking)
void playErrorSoundNB(unsigned long currentMillis)
{
unsigned long elapsed = currentMillis - soundStartTime;
if (currentSoundStep == 0 && elapsed >= 0)
{
tone(BUZZER_PIN, NOTE_C5, 100);
currentSoundStep = 1;
soundStartTime = currentMillis;
}
else if (currentSoundStep == 1 && elapsed >= 150)
{
tone(BUZZER_PIN, NOTE_C4, 300);
currentSoundStep = 2;
soundStartTime = currentMillis;
}
else if (currentSoundStep == 2 && elapsed >= 350)
{
// 효과음 종료
noTone(BUZZER_PIN);
isSoundPlaying = false;
soundToPlay = 0;
}
}