/**
* 두더지 게임 (Whack-A-Mole)
*
* 두더지가 3개의 구멍에서 랜덤하게 나타나고 사라지는 게임
* 구멍 위치: 왼쪽 위, 중앙 아래, 오른쪽 위
* 버튼을 눌러 두더지가 보일 때 망치로 두더지를 때리는 게임
*
* 하드웨어:
* - SSD1306 OLED 디스플레이 (I2C 연결)
* - 버튼 3개 (핀 2, 3, 4에 연결)
* https://wokwi.com/projects/428496069517124609
*
* 본 코드는 CLAUDE 모델이 수정했음
* MIT 라이센스를 가집니다.
*/
#include <U8g2lib.h>
#include <Wire.h> // I2C 라이브러리
//---------- 상수 정의 ----------
// 버튼 핀 정의
constexpr uint8_t BUTTON_PIN_1 = 2; // 왼쪽 상단 구멍용 버튼
constexpr uint8_t BUTTON_PIN_2 = 3; // 중앙 하단 구멍용 버튼
constexpr uint8_t BUTTON_PIN_3 = 4; // 오른쪽 상단 구멍용 버튼
constexpr uint8_t BUZZER_PIN = 11; // 부저 핀
// 화면 및 구멍 위치 상수
constexpr uint8_t SCREEN_WIDTH = 128;
constexpr uint8_t SCREEN_HEIGHT = 64;
constexpr uint8_t HOLE_RADIUS = 10;
constexpr uint8_t HOLE_OFFSET_X = 20;
constexpr uint8_t HOLE_OFFSET_Y = 20;
constexpr uint8_t HOLE1_X = HOLE_OFFSET_X;
constexpr uint8_t HOLE1_Y = HOLE_OFFSET_Y;
constexpr uint8_t HOLE2_X = SCREEN_WIDTH / 2;
constexpr uint8_t HOLE2_Y = SCREEN_HEIGHT - HOLE_OFFSET_Y;
constexpr uint8_t HOLE3_X = SCREEN_WIDTH - HOLE_OFFSET_X;
constexpr uint8_t HOLE3_Y = HOLE_OFFSET_Y;
const uint8_t HOLE_X[] = {HOLE1_X, HOLE2_X, HOLE3_X}; // 구멍 X좌표 배열
const uint8_t HOLE_Y[] = {HOLE1_Y, HOLE2_Y, HOLE3_Y}; // 구멍 Y좌표 배열
// 두더지 관련 상수
constexpr uint8_t MOLE_WIDTH = 12;
constexpr uint8_t MOLE_HEIGHT = 28;
constexpr uint8_t MOLE_RADIUS = MOLE_WIDTH / 2;
constexpr uint8_t MOLE_MIN_SCALE = 0; // 두더지 최소 크기 (0%)
constexpr uint8_t MOLE_MAX_SCALE = 100; // 두더지 최대 크기 (100%)
constexpr uint8_t MOLE_SCALE_SPEED = 5; // 두더지 크기 변화 속도
// 망치 관련 상수
constexpr int8_t HAMMER_START_ANGLE = -45; // 망치 시작 각도
constexpr int8_t HAMMER_HIT_ANGLE = 0; // 망치 타격 각도
constexpr uint8_t HAMMER_SWING_SPEED = 15; // 망치 회전 속도 (각도/프레임)
constexpr uint8_t HAMMER_HEAD_WIDTH = 10; // 망치 헤드 너비
constexpr uint8_t HAMMER_HEAD_HEIGHT = 8; // 망치 헤드 높이
constexpr uint8_t HAMMER_HANDLE_LENGTH = 25; // 망치 핸들 길이
// 타이밍 관련 상수
constexpr uint16_t FRAME_INTERVAL_MS = 30; // 프레임 간 간격 (밀리초)
constexpr uint16_t HAMMER_HIT_DURATION = 100; // 타격 상태 유지 시간 (밀리초)
constexpr uint16_t HIT_EFFECT_DURATION = 500; // 타격 효과 지속 시간 (밀리초)
constexpr uint16_t HIDING_TIME = 1000; // 숨어있는 상태 지속 시간 (밀리초)
constexpr uint16_t EMERGING_TIME = 500; // 나타나는 애니메이션 최대 시간 (밀리초)
constexpr uint16_t SHOWING_TIME = 1500; // 나와있는 상태 지속 시간 (밀리초) - 줄임
constexpr uint16_t HIDING_NOW_TIME = 300; // 숨어들어가는 애니메이션 최대 시간 (밀리초)
constexpr uint16_t RANDOM_DELAY_MAX = 1500; // 랜덤 추가 지연 시간 (밀리초)
constexpr uint16_t SPLASH_SCREEN_DURATION = 2000; // 초기화면 지속 시간 (밀리초)
constexpr uint8_t SPLASH_SCREEN_DELAY = 50; // 초기화면 딜레이 (밀리초)
// 게임 시간 관련 상수
constexpr uint32_t GAME_DURATION = 60000; // 게임 총 지속 시간 (1분 = 60,000ms)
constexpr uint16_t DIFFICULTY_INCREASE_INTERVAL = 20000; // 난이도 증가 간격 (20초)
constexpr uint8_t DIFFICULTY_INCREASE_PERCENT = 30; // 난이도 증가율 (30%)
// 그래픽 오프셋 상수
constexpr int8_t OFFSET_X = 0;
constexpr int8_t OFFSET_Y = 7; // 그래픽 세로 오프셋
// 부저 음향 관련 상수
// 음계 주파수 정의 (Hz)
constexpr uint16_t NOTE_C4 = 262;
constexpr uint16_t NOTE_D4 = 294;
constexpr uint16_t NOTE_E4 = 330;
constexpr uint16_t NOTE_F4 = 349;
constexpr uint16_t NOTE_G4 = 392;
constexpr uint16_t NOTE_A4 = 440;
constexpr uint16_t NOTE_B4 = 494;
constexpr uint16_t NOTE_C5 = 523;
constexpr uint16_t NOTE_D5 = 587;
constexpr uint16_t NOTE_E5 = 659;
constexpr uint16_t NOTE_F5 = 698;
constexpr uint16_t NOTE_G5 = 784;
// 부저 소리 지속 시간 상수
constexpr uint16_t HIT_SOUND_DURATION = 50; // 타격 소리 길이 (밀리초)
constexpr uint16_t GAME_SOUND_INTERVAL = 150; // 게임 사운드 간격 (밀리초)
// 두더지 애니메이션 상태 열거형
enum MoleState
{
HIDING, // 숨어있는 상태
EMERGING, // 나타나는 중
SHOWING, // 완전히 나와있는 상태
HIDING_NOW // 숨어들어가는 중
};
// 망치 애니메이션 상태 열거형
enum HammerState
{
HAMMER_IDLE, // 망치가 대기 상태
HAMMER_SWING, // 망치가 내려치는 중
HAMMER_HIT, // 망치가 타격한 상태
HAMMER_RETURN // 망치가 원위치로 돌아가는 중
};
// 게임 상태 열거형
enum GameState
{
SPLASH_SCREEN, // 초기화면 상태
GAME_PLAYING, // 게임 플레이 상태
GAME_OVER // 게임 종료 상태
};
//---------- 전역 변수 ----------
// OLED 디스플레이 객체 생성
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/U8X8_PIN_NONE, /* clock=*/SCL, /* data=*/SDA);
// 게임 상태 관련 변수
GameState gameState = SPLASH_SCREEN; // 초기 상태는 스플래시 화면
unsigned long splashScreenStartTime = 0; // 초기화면 시작 시간
unsigned long lastFrameTime = 0; // 마지막 프레임 업데이트 시간
unsigned long gameStartTime = 0; // 게임 시작 시간
uint8_t difficultyLevel = 0; // 난이도 레벨 (0-3, 각 20초마다 증가)
// 두더지 상태 관련 변수
MoleState moleState = HIDING;
int moleHeightScale = MOLE_MIN_SCALE; // 두더지 높이 스케일링 (0~100%)
unsigned long lastStateChangeTime = 0; // 마지막 상태 변경 시간
int activeHole = 1; // 현재 활성화된 구멍 (초기값은 가운데 구멍)
// 망치 애니메이션 변수
HammerState hammerStates[3] = {HAMMER_IDLE, HAMMER_IDLE, HAMMER_IDLE}; // 각 구멍의 망치 상태
int hammerAngles[3] = {HAMMER_START_ANGLE, HAMMER_START_ANGLE, HAMMER_START_ANGLE}; // 망치 회전 각도
unsigned long hammerAnimTimes[3] = {0, 0, 0}; // 각 망치의 애니메이션 시작 시간
bool buttonPressed[3] = {false, false, false}; // 버튼 눌림 상태 추적 배열
// 타격 효과 변수
bool moleHit = false; // 두더지가 타격되었는지 여부
unsigned long moleHitTime = 0; // 두더지가 타격된 시간
uint16_t hitCount = 0; // 두더지를 맞춘 횟수
/**
* 초기 설정 함수
*/
void setup()
{
// OLED 디스플레이 초기화
u8g2.begin();
// 랜덤 시드 초기화
randomSeed(analogRead(0)); // 버튼 핀 설정 - 내부 풀업 저항 활성화
pinMode(BUTTON_PIN_1, INPUT_PULLUP);
pinMode(BUTTON_PIN_2, INPUT_PULLUP);
pinMode(BUTTON_PIN_3, INPUT_PULLUP);
// 부저 핀 설정
pinMode(BUZZER_PIN, OUTPUT);
// 타이밍 초기화
lastFrameTime = millis();
}
/**
* 초기화면에 두더지 이미지 그리기 함수
*
* @param x 두더지 중심 X 좌표
* @param y 두더지 상단 Y 좌표
* @param scale 두더지 크기 비율 (0-255)
*/
void drawMoleImage(int16_t x, int16_t y, int16_t scale)
{
// 두더지의 크기 설정
int16_t moleWidth = 24 * scale / 100;
int16_t moleHeight = 30 * scale / 100;
int16_t headRadius = moleWidth / 2;
// 두더지 머리 그리기
u8g2.drawEllipse(x, y + headRadius, headRadius, headRadius, U8G2_DRAW_UPPER_LEFT | U8G2_DRAW_UPPER_RIGHT);
u8g2.setDrawColor(1);
u8g2.drawDisc(x, y + headRadius, headRadius - 1, U8G2_DRAW_UPPER_LEFT | U8G2_DRAW_UPPER_RIGHT);
// 얼굴 특징 그리기
u8g2.setDrawColor(0);
int16_t eyeY = y + headRadius + 2;
int16_t eyeLength = 2;
int16_t eyeOffsetX = 3 * scale / 100;
u8g2.drawLine(x - eyeOffsetX - eyeLength / 2, eyeY, x - eyeOffsetX + eyeLength / 2, eyeY);
u8g2.drawLine(x + eyeOffsetX - eyeLength / 2, eyeY, x + eyeOffsetX + eyeLength / 2, eyeY);
int16_t mouthY = eyeY + 3;
int16_t mouthLength = 4 * scale / 100;
u8g2.drawLine(x - mouthLength / 2, mouthY, x + mouthLength / 2, mouthY);
u8g2.setDrawColor(1);
// 몸통 그리기
int16_t bodyHeight = moleHeight - headRadius;
int16_t bodyX = x - moleWidth / 2;
int16_t bodyY = y + headRadius;
u8g2.drawBox(bodyX, bodyY, moleWidth, bodyHeight);
u8g2.drawLine(bodyX + moleWidth - 1, bodyY, bodyX + moleWidth - 1, y + moleHeight - 1);
u8g2.drawLine(bodyX + moleWidth, bodyY, bodyX + moleWidth, y + moleHeight - 1);
}
/**
* 초기화면 표시 함수
*/
void drawSplashScreen()
{
u8g2.clearBuffer();
// 게임 제목 표시
u8g2.setFont(u8g2_font_ncenB12_tr); // 큰 글꼴
u8g2.drawStr(5, 20, "Whack-A-Mole");
u8g2.setFont(u8g2_font_6x10_tr); // 작은 글꼴
u8g2.drawStr(5, 40, "Game");
// 오른쪽에 두더지 이미지 그리기
drawMoleImage(95, 30, 150); // x, y, scale(150%)
// 게임 시작 안내 메시지
u8g2.setFont(u8g2_font_5x8_tr);
u8g2.drawStr(5, 55, "Press any button to play!");
u8g2.sendBuffer();
}
/**
* 버튼 입력 처리 함수
*/
void handleButtonInput()
{
// 버튼 1 (왼쪽 상단 구멍)
bool btn1State = !digitalRead(BUTTON_PIN_1); // 내부 풀업 때문에 논리 반전
if (btn1State && !buttonPressed[0] && hammerStates[0] == HAMMER_IDLE)
{
hammerStates[0] = HAMMER_SWING;
hammerAnimTimes[0] = millis();
buttonPressed[0] = true;
}
else if (!btn1State)
{
buttonPressed[0] = false;
}
// 버튼 2 (중앙 하단 구멍)
bool btn2State = !digitalRead(BUTTON_PIN_2);
if (btn2State && !buttonPressed[1] && hammerStates[1] == HAMMER_IDLE)
{
hammerStates[1] = HAMMER_SWING;
hammerAnimTimes[1] = millis();
buttonPressed[1] = true;
}
else if (!btn2State)
{
buttonPressed[1] = false;
}
// 버튼 3 (오른쪽 상단 구멍)
bool btn3State = !digitalRead(BUTTON_PIN_3);
if (btn3State && !buttonPressed[2] && hammerStates[2] == HAMMER_IDLE)
{
hammerStates[2] = HAMMER_SWING;
hammerAnimTimes[2] = millis();
buttonPressed[2] = true;
}
else if (!btn3State)
{
buttonPressed[2] = false;
}
}
/**
* 망치 애니메이션 업데이트 함수
*/
void updateHammerAnimations()
{
unsigned long currentTime = millis();
// 각 망치 상태 업데이트
for (int i = 0; i < 3; i++)
{
switch (hammerStates[i])
{
case HAMMER_IDLE:
hammerAngles[i] = HAMMER_START_ANGLE; // 기본 각도
break;
case HAMMER_SWING:
// 망치를 내려치는 애니메이션
if (i == 0)
{
// 왼쪽 망치는 반대 방향으로 회전 (각도가 줄어듦)
hammerAngles[i] -= HAMMER_SWING_SPEED;
if (hammerAngles[i] <= HAMMER_HIT_ANGLE)
{
hammerAngles[i] = HAMMER_HIT_ANGLE; // 타격 위치
hammerStates[i] = HAMMER_HIT;
hammerAnimTimes[i] = currentTime; // 타이머 재설정
// 타격 성공 확인 - 현재 활성화된 구멍과 타격한 구멍이 일치하고
// 두더지가 보이는 상태 또는 충분히 나타난 상태(50% 이상)일 경우 히트 처리
if (i == activeHole && (moleState == SHOWING || (moleState == EMERGING && moleHeightScale > 50)) && !moleHit)
{
moleHit = true;
moleHitTime = currentTime;
// 타격 성공 시 두더지는 바로 숨어들어가기 시작
moleState = HIDING_NOW;
lastStateChangeTime = currentTime;
// 히트 카운트 증가
hitCount++;
// 타격 효과음 재생
playHitSound();
}
}
}
else
{
// 중앙, 오른쪽 망치는 기존 방식대로 회전 (각도가 증가)
hammerAngles[i] += HAMMER_SWING_SPEED;
if (hammerAngles[i] >= HAMMER_HIT_ANGLE)
{
hammerAngles[i] = HAMMER_HIT_ANGLE; // 타격 위치
hammerStates[i] = HAMMER_HIT;
hammerAnimTimes[i] = currentTime; // 타이머 재설정
// 타격 성공 확인 - 현재 활성화된 구멍과 타격한 구멍이 일치하고
// 두더지가 보이는 상태 또는 충분히 나타난 상태(50% 이상)일 경우 히트 처리
if (i == activeHole && (moleState == SHOWING || (moleState == EMERGING && moleHeightScale > 50)) && !moleHit)
{
moleHit = true;
moleHitTime = currentTime;
// 타격 성공 시 두더지는 바로 숨어들어가기 시작
moleState = HIDING_NOW;
lastStateChangeTime = currentTime;
// 히트 카운트 증가
hitCount++;
// 타격 효과음 재생
playHitSound();
}
}
}
break;
case HAMMER_HIT:
// 타격 상태 유지
if (currentTime - hammerAnimTimes[i] > HAMMER_HIT_DURATION)
{
hammerStates[i] = HAMMER_RETURN;
}
break;
case HAMMER_RETURN:
// 망치가 원위치로 돌아감
hammerAngles[i] -= HAMMER_SWING_SPEED;
if (hammerAngles[i] <= HAMMER_START_ANGLE)
{
hammerAngles[i] = HAMMER_START_ANGLE; // 기본 위치로 복원
hammerStates[i] = HAMMER_IDLE;
}
break;
}
}
}
/**
* 두더지 상태 업데이트 함수
*/
void updateMoleState()
{
unsigned long currentTime = millis();
// 현재 난이도 기반 속도 계수 계산
float speedFactor = getAnimationSpeedFactor();
// 머무는 시간에 적용할 더 강화된 난이도 계수 (기본 속도 계수보다 더 가파르게 감소)
float difficultyFactor = 1.0 + (difficultyLevel * (DIFFICULTY_INCREASE_PERCENT + 10) / 100.0);
// 난이도에 따라 조정된 두더지 애니메이션 속도
int adjustedScaleSpeed = round(MOLE_SCALE_SPEED * speedFactor);
// 상태에 따른 두더지 크기 업데이트
switch (moleState)
{
case HIDING:
moleHeightScale = MOLE_MIN_SCALE; // 구멍에 완전히 숨은 상태 (최소 높이)
// 난이도에 따라 두더지가 더 빨리 등장 (기다리는 시간 감소)
if (currentTime - lastStateChangeTime > HIDING_TIME / speedFactor + random(RANDOM_DELAY_MAX / speedFactor))
{
moleState = EMERGING;
lastStateChangeTime = currentTime;
// 다음 애니메이션을 위해 랜덤 구멍 선택
activeHole = random(0, 3);
}
break;
case EMERGING:
// 서서히 늘어나는 애니메이션 - 난이도에 따라 속도 증가
moleHeightScale += adjustedScaleSpeed;
if (moleHeightScale >= MOLE_MAX_SCALE)
{
moleHeightScale = MOLE_MAX_SCALE; // 두더지 완전히 드러나게 고정
moleState = SHOWING;
lastStateChangeTime = currentTime;
}
break;
case SHOWING:
// 두더지가 완전히 나와 있는 상태 유지 (최대 높이)
// 난이도에 따라 두더지가 더 짧은 시간 동안 보임 (보이는 시간 감소)
// 레벨이 증가할수록 머무는 시간이 더 빠르게 감소
if (currentTime - lastStateChangeTime > SHOWING_TIME / difficultyFactor)
{
moleState = HIDING_NOW;
lastStateChangeTime = currentTime;
}
break;
case HIDING_NOW:
// 서서히 줄어드는 애니메이션 - 난이도에 따라 속도 증가
moleHeightScale -= adjustedScaleSpeed;
if (moleHeightScale <= MOLE_MIN_SCALE || (currentTime - lastStateChangeTime > HIDING_NOW_TIME / speedFactor))
{
moleHeightScale = MOLE_MIN_SCALE;
moleState = HIDING;
lastStateChangeTime = currentTime;
}
break;
}
}
/**
* 현재 두더지 애니메이션 속도 계수 계산 함수
*
* 난이도 레벨에 따라 속도를 증가시킴:
* - 레벨 0: 100% (원래 속도)
* - 레벨 1: 130% (30% 증가)
* - 레벨 2: 160% (60% 증가)
* - 레벨 3: 190% (90% 증가)
*
* @return 속도 계수 (1.0 = 원래 속도, 1.3 = 30% 빠름, 등)
*/
float getAnimationSpeedFactor()
{
// 기본 속도(100%)에 난이도 레벨 * 30%만큼 속도 증가
return 1.0 + (difficultyLevel * DIFFICULTY_INCREASE_PERCENT / 100.0);
}
/**
* 망치 그리기 함수
*
* @param holeIndex 구멍 인덱스 (0-2)
*/
void drawHammer(int holeIndex)
{
int16_t hammerAngle = hammerAngles[holeIndex];
int16_t holeCenterX = HOLE_X[holeIndex] + OFFSET_X;
int16_t holeCenterY = HOLE_Y[holeIndex] + OFFSET_Y;
// 망치의 시작점(피벗) 위치
int16_t hammerPivotX;
int16_t hammerPivotY = holeCenterY;
// 왼쪽 구멍(인덱스 0)의 경우 망치를 반대쪽(오른쪽)에 배치
if (holeIndex == 0)
{
// 왼쪽 구멍에는 망치를 오른쪽에 배치
if (holeIndex == activeHole && (moleState == SHOWING || moleState == EMERGING))
{
// 두더지가 보이는 구멍일 경우 타격 지점을 두더지 머리에 맞추기
hammerPivotX = holeCenterX + 35; // 오른쪽에 위치
}
else
{
// 두더지가 없는 구멍일 경우 기본 위치
hammerPivotX = holeCenterX + 30; // 오른쪽에 위치
}
}
else
{
// 중앙, 오른쪽 구멍은 기존과 동일하게 왼쪽에 망치 배치
if (holeIndex == activeHole && (moleState == SHOWING || moleState == EMERGING))
{
// 두더지가 보이는 구멍일 경우 타격 지점을 두더지 머리에 맞추기
hammerPivotX = holeCenterX - 35; // 원래 -25에서 -10 (왼쪽으로 이동)
}
else
{
// 두더지가 없는 구멍일 경우 기본 위치
hammerPivotX = holeCenterX - 30; // 원래 -20에서 -10 (왼쪽으로 이동)
}
} // 망치 핸들 회전 위치 계산 (삼각함수 사용)
float angleRad;
if (holeIndex == 0)
{
// 왼쪽 구멍은 각도를 뒤집어서 계산 (180도 - 각도)
angleRad = radians(180 - hammerAngle);
}
else
{
// 기존 각도 계산 방식
angleRad = radians(hammerAngle);
}
int16_t handleEndX = hammerPivotX + HAMMER_HANDLE_LENGTH * cos(angleRad);
int16_t handleEndY = hammerPivotY + HAMMER_HANDLE_LENGTH * sin(angleRad);
// 망치 핸들 그리기
u8g2.drawLine(hammerPivotX, hammerPivotY, handleEndX, handleEndY); // 망치 헤드 그리기 (핸들 끝에 사각형으로 표현)
// 망치 헤드의 각도는 핸들과 수직
float headAngleRad;
if (holeIndex == 0)
{
// 왼쪽 구멍은 망치 헤드 각도도 반대로 (90도 더함, 아니면 빼는 방향 변경)
headAngleRad = angleRad + radians(90);
}
else
{
// 기존 계산 방식
headAngleRad = angleRad - radians(90);
}
// 망치 헤드의 네 꼭지점 계산
int16_t cornerX1 = handleEndX + (HAMMER_HEAD_WIDTH / 2) * cos(headAngleRad);
int16_t cornerY1 = handleEndY + (HAMMER_HEAD_WIDTH / 2) * sin(headAngleRad);
int16_t cornerX2 = handleEndX - (HAMMER_HEAD_WIDTH / 2) * cos(headAngleRad);
int16_t cornerY2 = handleEndY - (HAMMER_HEAD_WIDTH / 2) * sin(headAngleRad);
// 망치 헤드 길이 방향의 벡터 계산
float headLengthAngleRad = angleRad;
int16_t cornerX3 = cornerX2 + HAMMER_HEAD_HEIGHT * cos(headLengthAngleRad);
int16_t cornerY3 = cornerY2 + HAMMER_HEAD_HEIGHT * sin(headLengthAngleRad);
int16_t cornerX4 = cornerX1 + HAMMER_HEAD_HEIGHT * cos(headLengthAngleRad);
int16_t cornerY4 = cornerY1 + HAMMER_HEAD_HEIGHT * sin(headLengthAngleRad); // 망치 헤드 그리기 (사각형)
u8g2.drawLine(cornerX1, cornerY1, cornerX2, cornerY2);
u8g2.drawLine(cornerX2, cornerY2, cornerX3, cornerY3);
u8g2.drawLine(cornerX3, cornerY3, cornerX4, cornerY4);
u8g2.drawLine(cornerX4, cornerY4, cornerX1, cornerY1); // 망치 헤드 채우기 - 모든 망치에 대해 픽셀 단위로 채우기 방식 적용
// 좌표를 정확히 계산하여 사각형 내부를 채움
int minX = min(min(cornerX1, cornerX2), min(cornerX3, cornerX4));
int minY = min(min(cornerY1, cornerY2), min(cornerY3, cornerY4));
int maxX = max(max(cornerX1, cornerX2), max(cornerX3, cornerX4));
int maxY = max(max(cornerY1, cornerY2), max(cornerY3, cornerY4));
// 내부 영역 채우기 - 약간 작게 채워서 테두리와 겹치지 않게
for (int y = minY + 1; y < maxY; y++)
{
for (int x = minX + 1; x < maxX; x++)
{
u8g2.drawPixel(x, y);
}
}
}
/**
* 게임 화면 그리기 함수
*/
void drawGameScreen()
{
unsigned long currentTime = millis();
u8g2.clearBuffer();
// 3개의 구멍 그리기 (먼저 배경)
for (int i = 0; i < 3; i++)
{
u8g2.drawCircle(HOLE_X[i] + OFFSET_X, HOLE_Y[i] + OFFSET_Y, HOLE_RADIUS, U8G2_DRAW_ALL);
}
// 현재 활성화된 구멍에 두더지 위치 설정
int16_t moleCenterX = HOLE_X[activeHole] + OFFSET_X;
int16_t holeY = HOLE_Y[activeHole] + OFFSET_Y;
// 두더지의 기준점을 구멍 바닥으로 설정 (구멍의 가장 하단부)
int16_t moleBaseY = holeY + HOLE_RADIUS - 5; // 구멍 하단에서 약간 위로(-5)
// 두더지의 현재 높이 계산 (스케일 기반)
int16_t currentMoleHeight = (MOLE_HEIGHT * moleHeightScale) / 100;
// 두더지 그리기에 필요한 위치 계산 - 기준점(아래)에서 위로
int16_t moleTopY = moleBaseY - currentMoleHeight;
int16_t moleLeftX = moleCenterX - MOLE_WIDTH / 2;
// 숨어있는 상태가 아닐 때만 두더지 그리기
if (moleHeightScale > MOLE_MIN_SCALE)
{
// 머리 크기 계산 (높이에 비례하여 조절)
int16_t headRadius = max(1, (MOLE_RADIUS * moleHeightScale) / 100);
// 타격 효과 - 두더지가 타격되었고 효과 지속 시간이 지나지 않았으면 깜빡임 효과
bool showMole = true;
if (moleHit && (currentTime - moleHitTime < HIT_EFFECT_DURATION))
{
// 깜빡임 효과 (짝수/홀수 프레임마다 그리기)
showMole = ((currentTime / 100) % 2 == 0);
}
else if (currentTime - moleHitTime >= HIT_EFFECT_DURATION)
{
moleHit = false; // 효과 시간이 지나면 타격 상태 해제
}
if (showMole)
{
// 머리 그리기 (두더지의 상단부)
u8g2.drawEllipse(moleCenterX, moleTopY + headRadius, headRadius, headRadius,
U8G2_DRAW_UPPER_LEFT | U8G2_DRAW_UPPER_RIGHT);
u8g2.setDrawColor(1);
u8g2.drawDisc(moleCenterX, moleTopY + headRadius, headRadius - 1,
U8G2_DRAW_UPPER_LEFT | U8G2_DRAW_UPPER_RIGHT);
// 얼굴 특징 그리기 (머리가 충분히 보이는 경우에만)
if (moleHeightScale > 50)
{
u8g2.setDrawColor(0);
int16_t eyeY = moleTopY + headRadius + 2;
int16_t eyeLength = 2;
int16_t eyeOffsetX = 3;
u8g2.drawLine(moleCenterX - eyeOffsetX - eyeLength / 2, eyeY,
moleCenterX - eyeOffsetX + eyeLength / 2, eyeY);
u8g2.drawLine(moleCenterX + eyeOffsetX - eyeLength / 2, eyeY,
moleCenterX + eyeOffsetX + eyeLength / 2, eyeY);
int16_t mouthY = eyeY + 3;
int16_t mouthLength = 4;
u8g2.drawLine(moleCenterX - mouthLength / 2, mouthY,
moleCenterX + mouthLength / 2, mouthY);
u8g2.setDrawColor(1);
}
// 몸통 그리기 (두더지의 하단부)
int16_t bodyHeight = max(1, currentMoleHeight - headRadius);
u8g2.drawBox(moleLeftX, moleTopY + headRadius, MOLE_WIDTH, bodyHeight);
u8g2.drawLine(moleLeftX + MOLE_WIDTH - 1, moleTopY + headRadius,
moleLeftX + MOLE_WIDTH - 1, moleTopY + currentMoleHeight - 1);
u8g2.drawLine(moleLeftX + MOLE_WIDTH, moleTopY + headRadius,
moleLeftX + MOLE_WIDTH, moleTopY + currentMoleHeight - 1);
}
}
// 구멍(원)을 두더지 위에 다시 그려서 마스킹 효과 (구멍 밖 부분 가리기)
u8g2.drawCircle(HOLE_X[activeHole] + OFFSET_X, HOLE_Y[activeHole] + OFFSET_Y,
HOLE_RADIUS, U8G2_DRAW_ALL); // 각 구멍에 망치 그리기
for (int i = 0; i < 3; i++)
{
// 망치가 대기 상태가 아닐 때만 그림
if (hammerStates[i] != HAMMER_IDLE)
{
drawHammer(i);
}
}
// HIT 카운트 표시
u8g2.setFont(u8g2_font_6x10_tr); // HIT 텍스트용 폰트 (구멍 크기의 약 1/2)
u8g2.drawStr((SCREEN_WIDTH - u8g2.getStrWidth("HIT!!")) / 2, SCREEN_HEIGHT - 55, "HIT!!");
// 히트 카운트 숫자 표시
u8g2.setFont(u8g2_font_ncenB12_tr); // 카운트 숫자용 폰트 (구멍 크기 정도)
// 숫자를 문자열로 변환
char countStr[5]; // 최대 9999까지 표시 가능
sprintf(countStr, "%d", hitCount);
// 화면 중앙에 카운트 표시
u8g2.drawStr((SCREEN_WIDTH - u8g2.getStrWidth(countStr)) / 2, SCREEN_HEIGHT - 39, countStr);
u8g2.sendBuffer();
}
/**
* 초기화면 처리 함수
*/
bool handleSplashScreen()
{
unsigned long currentTime = millis();
// 초기화면 처음 진입 시 시간 설정
if (splashScreenStartTime == 0)
{
splashScreenStartTime = currentTime;
drawSplashScreen();
}
// 초기화면 표시 시간이 지나면 게임 시작
else if (currentTime - splashScreenStartTime > SPLASH_SCREEN_DURATION)
{
return false; // 스플래시 화면 종료
}
// 버튼 입력 감지
bool btn1State = !digitalRead(BUTTON_PIN_1);
bool btn2State = !digitalRead(BUTTON_PIN_2);
bool btn3State = !digitalRead(BUTTON_PIN_3);
// 아무 버튼이나 누르면 초기화면 건너뛰기
if (btn1State || btn2State || btn3State)
{
return false; // 스플래시 화면 종료
}
delay(SPLASH_SCREEN_DELAY); // 초기화면에서는 약간의 딜레이
return true; // 스플래시 화면 유지
}
/**
* 게임 오버 화면 표시 함수
*/
void drawGameOverScreen()
{
u8g2.clearBuffer();
// 게임 오버 메시지 표시
u8g2.setFont(u8g2_font_ncenB12_tr); // 큰 글꼴
u8g2.drawStr(10, 20, "Game Over");
// 최종 점수 표시
u8g2.setFont(u8g2_font_6x10_tr); // 작은 글꼴
char scoreText[16];
sprintf(scoreText, "Final Score: %d", hitCount);
u8g2.drawStr(15, 40, scoreText);
// 재시작 안내 메시지
u8g2.setFont(u8g2_font_5x8_tr);
u8g2.drawStr(5, 55, "Press any button to play again");
u8g2.sendBuffer();
}
/**
* 게임 오버 상태 처리 함수
*
* @return 게임 오버 화면을 계속 표시할지 여부
*/
bool handleGameOver()
{
// 게임 오버 화면 표시
drawGameOverScreen();
// 버튼 입력 감지
bool btn1State = !digitalRead(BUTTON_PIN_1);
bool btn2State = !digitalRead(BUTTON_PIN_2);
bool btn3State = !digitalRead(BUTTON_PIN_3); // 아무 버튼이나 누르면 게임 재시작
if (btn1State || btn2State || btn3State)
{
// 게임 상태 초기화
gameState = GAME_PLAYING;
gameStartTime = millis();
difficultyLevel = 0;
moleState = HIDING;
moleHeightScale = MOLE_MIN_SCALE;
lastStateChangeTime = millis();
hitCount = 0; // 점수 초기화
// 게임 시작 음악 재생
playStartMusic();
for (int i = 0; i < 3; i++)
{
hammerStates[i] = HAMMER_IDLE;
hammerAngles[i] = HAMMER_START_ANGLE;
}
return false; // 게임 오버 화면 종료
}
delay(SPLASH_SCREEN_DELAY); // 약간의 딜레이
return true; // 게임 오버 화면 유지
}
/**
* 메인 게임 루프 함수
*/
void gameLoop()
{
unsigned long currentTime = millis();
unsigned long gameElapsedTime = currentTime - gameStartTime; // 게임 제한 시간 체크 (1분)
if (gameElapsedTime >= GAME_DURATION)
{
gameState = GAME_OVER;
// 게임 종료 음악 재생
playGameOverMusic();
return;
}
// 난이도 레벨 업데이트 (20초마다)
uint8_t newDifficultyLevel = min(3, gameElapsedTime / DIFFICULTY_INCREASE_INTERVAL);
if (newDifficultyLevel != difficultyLevel)
{
difficultyLevel = newDifficultyLevel;
}
// 비차단 방식의 프레임 업데이트 (고정 시간 간격)
if (currentTime - lastFrameTime >= FRAME_INTERVAL_MS)
{
lastFrameTime = currentTime;
// 버튼 입력 처리
handleButtonInput();
// 망치 애니메이션 상태 업데이트
updateHammerAnimations();
// 두더지 상태 업데이트
updateMoleState();
// 게임 화면 그리기
drawGameScreen();
}
}
/**
* 메인 루프 함수
*/
void loop()
{
// 게임 상태에 따라 다른 처리
if (gameState == SPLASH_SCREEN)
{
// 초기화면 처리
bool stayInSplash = handleSplashScreen();
if (!stayInSplash)
{
gameState = GAME_PLAYING;
gameStartTime = millis(); // 게임 시작 시간 기록
difficultyLevel = 0; // 난이도 초기화
lastStateChangeTime = millis(); // 게임 시작 시간 기록
hitCount = 0; // 타격 카운트 초기화
// 게임 시작 음악 재생
playStartMusic();
}
}
else if (gameState == GAME_PLAYING)
{
// 게임 플레이 처리
gameLoop();
}
else if (gameState == GAME_OVER)
{
// 게임 오버 처리
handleGameOver();
}
}
/**
* 부저로 소리 재생 함수
*
* @param pin 부저가 연결된 핀 번호
* @param frequency 재생할 음의 주파수 (Hz)
* @param duration 재생 시간 (밀리초)
*/
void playTone(uint8_t pin, uint16_t frequency, uint16_t duration)
{
tone(pin, frequency, duration);
delay(duration);
noTone(pin);
}
/**
* 게임 시작 배경음 재생 함수
*/
void playStartMusic()
{
// 게임 시작 배경음 (간단한 멜로디)
playTone(BUZZER_PIN, NOTE_C4, 150);
delay(50);
playTone(BUZZER_PIN, NOTE_E4, 150);
delay(50);
playTone(BUZZER_PIN, NOTE_G4, 150);
delay(50);
playTone(BUZZER_PIN, NOTE_C5, 300);
}
/**
* 두더지 타격 효과음 재생 함수
*/
void playHitSound()
{
// 높은 음의 짧은 효과음
playTone(BUZZER_PIN, NOTE_G5, HIT_SOUND_DURATION);
}
/**
* 게임 종료 배경음 재생 함수
*/
void playGameOverMusic()
{
// 게임 오버 배경음 (하향 멜로디)
playTone(BUZZER_PIN, NOTE_C5, 150);
delay(50);
playTone(BUZZER_PIN, NOTE_G4, 150);
delay(50);
playTone(BUZZER_PIN, NOTE_E4, 150);
delay(50);
playTone(BUZZER_PIN, NOTE_C4, 300);
}