
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define OLED_ADDR 0x3C
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// ---------------- Buttons (INPUT_PULLUP, 눌림=LOW) ----------------
const int BTN_LEFT = 2; // 확률: -10% / 게임: 가위
const int BTN_OK = 3; // 확률: 확인 / 게임: 바위
const int BTN_RIGHT = 4; // 확률: +10% / 게임: 보
struct Btn {
int pin;
bool lastStable;
bool lastRead;
unsigned long lastChangeMs;
bool pressedEvent; // HIGH->LOW 에지
};
Btn bLeft {BTN_LEFT, true, true, 0, false};
Btn bOk {BTN_OK, true, true, 0, false};
Btn bRight {BTN_RIGHT, true, true, 0, false};
const unsigned long DEBOUNCE_MS = 20;
// ---------------- Game State ----------------
enum GameState { SET_PROB, READY, WAIT_PLAYER, ANIMATING, REVEAL };
GameState state = SET_PROB;
enum Move { ROCK = 0, PAPER = 1, SCISSORS = 2, NONE = 3 };
enum Outcome { P_WIN, DRAW, C_WIN };
// ---------------- Config / Runtime ----------------
int pWin = 50; // CPU 승률(%), 10% 단위로 증감
// 통계
unsigned long rounds = 0;
unsigned long playerWins = 0;
unsigned long draws = 0;
unsigned long compWins = 0;
Move playerMove = NONE;
Move compMove = NONE;
Outcome outcome;
// 애니메이션
unsigned long animStartMs = 0;
const unsigned long animDurationMs = 3000; // 3초
const unsigned long animFrameMs = 120; // 프레임 간격
unsigned long lastFrameMs = 0;
int animFrame = 0; // 0:R,1:P,2:S 반복
// ---------------- 도우미 ----------------
void updateBtn(Btn &b) {
bool r = digitalRead(b.pin);
unsigned long now = millis();
if (r != b.lastRead) { b.lastChangeMs = now; b.lastRead = r; }
b.pressedEvent = false;
if ((now - b.lastChangeMs) > DEBOUNCE_MS) {
if (r != b.lastStable) {
if (b.lastStable == HIGH && r == LOW) b.pressedEvent = true;
b.lastStable = r;
}
}
}
bool pressed(Btn &b){ return b.pressedEvent; }
void drawCentered(const char* s, int y, int size=1) {
int16_t x1, y1; uint16_t w, h;
display.setTextSize(size);
display.getTextBounds(s, 0, y, &x1, &y1, &w, &h);
display.setCursor((SCREEN_WIDTH - w)/2, y);
display.print(s);
}
// ---------------- 이모티콘 스타일 손모양 (도형 기반) ----------------
const int ICON_SZ = 24; // 아이콘 캔버스 24x24
// 손목(팔) 표현 보조
void drawWrist(int x, int y, int w) {
display.fillRoundRect(x + (ICON_SZ - w)/2, y + ICON_SZ - 6, w, 6, 2, SSD1306_WHITE);
}
// ✊ 바위 (세로형)
void drawEmojiRock(int x, int y) {
// 손바닥(둥근 사각형)
display.fillRoundRect(x + 3, y + 6, 18, 16, 6, SSD1306_WHITE);
// 윗쪽 볼륨
display.fillCircle(x + 6, y + 6, 3, SSD1306_WHITE);
display.fillCircle(x + 12, y + 5, 4, SSD1306_WHITE);
display.fillCircle(x + 18, y + 6, 3, SSD1306_WHITE);
drawWrist(x, y, 10);
}
// ✋ 보 (세로형)
void drawEmojiPaper(int x, int y) {
// 손바닥
display.fillRoundRect(x + 4, y + 8, 16, 14, 4, SSD1306_WHITE);
// 손가락 4개
display.fillRoundRect(x + 6, y + 1, 3, 9, 1, SSD1306_WHITE);
display.fillRoundRect(x + 10, y + 0, 3, 10, 1, SSD1306_WHITE);
display.fillRoundRect(x + 14, y + 1, 3, 9, 1, SSD1306_WHITE);
display.fillRoundRect(x + 18, y + 2, 3, 8, 1, SSD1306_WHITE);
// 엄지
display.fillRoundRect(x + 2, y + 11, 4, 6, 2, SSD1306_WHITE);
drawWrist(x, y, 12);
}
// ✌ 가위 (세로형)
void drawEmojiScissors(int x, int y) {
// 손바닥
display.fillRoundRect(x + 6, y + 12, 12, 10, 4, SSD1306_WHITE);
// 두 손가락(V자)
display.fillRoundRect(x + 7, y + 1, 4, 12, 2, SSD1306_WHITE);
display.fillRoundRect(x + 13, y + 1, 4, 12, 2, SSD1306_WHITE);
// 엄지 삼각형
display.fillTriangle(
x + 6, y + 16,
x + 12, y + 12,
x + 6, y + 12,
SSD1306_WHITE
);
// V자 보조선(옵션)
display.drawLine(x + 9, y + 1, x + 9, y + 12, SSD1306_WHITE);
display.drawLine(x + 15, y + 1, x + 15, y + 12, SSD1306_WHITE);
drawWrist(x, y, 10);
}
void drawEmojiHand(Move move, int x, int y) {
switch (move) {
case ROCK: drawEmojiRock(x, y); break;
case PAPER: drawEmojiPaper(x, y); break;
case SCISSORS: drawEmojiScissors(x, y); break;
default: break;
}
}
// ---------------- 게임 로직 ----------------
Move moveThatBeats(Move m) {
if (m == ROCK) return PAPER;
if (m == PAPER) return SCISSORS;
return ROCK;
}
Move moveThatLosesTo(Move m) {
if (m == ROCK) return SCISSORS;
if (m == PAPER) return ROCK;
return PAPER;
}
void drawProbScreen() {
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
drawCentered("RPS Game", 0, 1);
display.setCursor(0, 16); display.setTextSize(1);
display.print("Set CPU Win Prob: "); display.print(pWin); display.println("%");
drawCentered("Left:-10% Right:+10%", 36, 1);
drawCentered("OK: Confirm & Start", 48, 1);
display.display();
}
void drawReady() {
display.clearDisplay();
display.setTextColor(SSD1306_WHITE); display.setTextSize(1);
drawCentered("Choose Your Hand", 0, 1);
display.setCursor(0, 16);
display.print("CPU win prob: "); display.print(pWin); display.println("%");
display.println("L=Scissors O=Rock R=Paper");
display.print("Rounds: "); display.print(rounds);
display.print(" P:"); display.print(playerWins);
display.print(" D:"); display.print(draws);
display.print(" C:"); display.print(compWins);
display.display();
}
void drawAnimatingFrame() {
display.clearDisplay();
display.setTextColor(SSD1306_WHITE); display.setTextSize(1);
drawCentered("Rock... Paper... Scissors...", 0, 1);
// 플레이어 고정
drawEmojiHand(playerMove, 22, 20);
display.setCursor(20, 46); display.print("YOU");
// CPU 순환
Move cycle = (Move)(animFrame % 3); // 0:R,1:P,2:S
drawEmojiHand(cycle, 82, 20);
display.setCursor(84, 46); display.print("CPU");
display.display();
}
void drawReveal() {
display.clearDisplay();
display.setTextColor(SSD1306_WHITE); display.setTextSize(1);
drawCentered("Result", 0, 1);
drawEmojiHand(playerMove, 22, 20);
drawEmojiHand(compMove, 82, 20);
display.setCursor(20, 46); display.print("YOU");
display.setCursor(84, 46); display.print("CPU");
const char* res =
(outcome == P_WIN) ? "YOU WIN" :
(outcome == C_WIN) ? "YOU LOSE" : "DRAW";
drawCentered(res, 56, 1);
display.display();
}
void decideComputer(Move pm) {
// pWin%는 CPU 승률, 나머지는 무승부/플레이어승을 50:50
int p_cwin = pWin;
int p_draw = (100 - pWin)/2;
int r = random(0, 100); // 0..99
if (r < p_cwin) {
compMove = moveThatBeats(pm);
outcome = C_WIN;
} else if (r < p_cwin + p_draw) {
compMove = pm;
outcome = DRAW;
} else {
compMove = moveThatLosesTo(pm);
outcome = P_WIN;
}
}
// ---------------- Setup / Loop ----------------
void setup() {
pinMode(BTN_LEFT, INPUT_PULLUP);
pinMode(BTN_OK, INPUT_PULLUP);
pinMode(BTN_RIGHT, INPUT_PULLUP);
if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) {
while (true) { delay(1000); } // 디스플레이 초기화 실패 시 대기
}
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
randomSeed(analogRead(A0)); // 시드 확보
drawProbScreen();
}
void loop() {
updateBtn(bLeft);
updateBtn(bOk);
updateBtn(bRight);
switch (state) {
case SET_PROB: {
bool changed = false;
if (pressed(bLeft)) { if (pWin > 0) { pWin -= 10; if (pWin < 0) pWin = 0; changed = true; } }
if (pressed(bRight)) { if (pWin < 100) { pWin += 10; if (pWin > 100) pWin = 100; changed = true; } }
if (changed) drawProbScreen();
if (pressed(bOk)) { state = READY; drawReady(); }
} break;
case READY: {
// 아무 버튼 눌리면 선택 대기 상태로 진입
if (pressed(bLeft) || pressed(bOk) || pressed(bRight)) state = WAIT_PLAYER;
} [[fallthrough]];
case WAIT_PLAYER: {
Move sel = NONE;
if (pressed(bLeft)) sel = SCISSORS;
if (pressed(bOk)) sel = ROCK;
if (pressed(bRight)) sel = PAPER;
if (sel != NONE) {
playerMove = sel;
animStartMs = millis();
lastFrameMs = animStartMs;
animFrame = 0;
state = ANIMATING;
} else if (state == WAIT_PLAYER) {
// 필요 시 안내 재표시 가능
// drawReady();
}
} break;
case ANIMATING: {
unsigned long now = millis();
if (now - lastFrameMs >= animFrameMs) {
animFrame++;
lastFrameMs = now;
drawAnimatingFrame();
}
if (now - animStartMs >= animDurationMs) {
decideComputer(playerMove);
rounds++;
if (outcome == P_WIN) playerWins++;
else if (outcome == DRAW) draws++;
else compWins++;
drawReveal();
state = REVEAL;
}
} break;
case REVEAL: {
// 아무 버튼이나 누르면 다음 라운드
if (pressed(bLeft) || pressed(bOk) || pressed(bRight)) {
state = WAIT_PLAYER;
drawReady();
}
} break;
}
delay(5);
}

(주)인투피온
대표:소영삼 사업자등록번호:113-86-29364 [사업자정보확인] 통신판매신고:2015-서울구로-1028
본사 : 서울 구로구 경인로 53길 90 STX W-Tower 1307호
매장 : 서울 구로구 경인로 53길 15 중앙유통단지 다동 4403호
고객상담
팩스번호: 02-6124-4242 이메일: info@intopion.com
* 재고 확인, 배송, 기술문의는 바로 답변이 어려우니, 가급적 카카오톡 플러스친구 [인투피온] 이용 부탁드립니다 *
개인정보관리책임자 : 이성민 / Hosting Provider : ㈜가비아씨엔에스