바이브 코딩 개념 및 실습으로 너구리 달리기 만들기 등

소개

바이브 코딩 개념 및 실습으로 게임 만들기 학습을 했습니다.

Zoom 정원이 찼다고 해서 오늘은 그냥 쉬는 날인가 했는데, 입장이 가능하다고 해서 후다닥 들어가서 오늘도 알찬 스터디를 했습니다.

일단 너무 재미있었습니다.

오류가 한가득 나오긴 했습니다만 ㅋ

진행 방법

어떤 도구를 사용했고, 어떻게 활용하셨나요?

클로드 코드, 깃, 노드를 설치하고 window shell에서 코드를 실행하는 실습이었습니다. 명령어를 좀 배워야 해서 익숙하진 않아 조금 헤맸습니다.

Tip: 사용한 프롬프트 전문을 꼭 포함하고, 내용을 짧게 소개해 주세요.

한국어 텍스트가 적힌 블루 스크린

개발자F는 시키는대로 작성했는데도 아주 간단하게 구현이 되어서 신기했습니다.

한국어 프로그램 스크린샷

본인이 만들고 싶은 추억의 게임을 만들어 보라고 하셔서 저는 마장을 만들어 보았습니다.

평소 좋아하는 게임 ㅋ

Tip: 활용 이미지나 캡처 화면을 꼭 남겨주세요.

한국어로 된 게임 스크린샷

첫 번째 구현한 너구리 달리기입니다.

아직 skill은 사용해보지 못 했습니다.

약간 무서워서 ㅋ

마작 코리아 - 스크린샷

두 번째 만든 것은 마장입니다.

Tip: 코드 전문은 코드블록에 감싸서 작성해주세요. ( / 을 눌러 '코드 블록'을 선택)


<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>마작 솔리테어</title>
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body {
      background: radial-gradient(ellipse at center, #1a3a20 0%, #0d2010 100%);
      display: flex;
      flex-direction: column;
      align-items: center;
      padding: 20px;
      min-height: 100vh;
      font-family: 'Segoe UI', sans-serif;
      color: #fff;
    }

    header { text-align: center; margin-bottom: 14px; }
    header h1 {
      font-size: 2rem;
      color: #f0c040;
      text-shadow: 0 0 14px rgba(240,192,64,0.6);
      letter-spacing: 2px;
    }
    header p { color: #aaa; font-size: 0.85rem; margin-top: 3px; }

    #stats {
      display: flex;
      gap: 28px;
      margin-bottom: 10px;
      font-size: 0.95rem;
    }
    .stat { color: #ccc; }
    .stat strong { color: #f0c040; font-size: 1.15em; }

    #controls {
      display: flex;
      gap: 10px;
      margin-bottom: 10px;
    }
    button {
      padding: 7px 16px;
      font-size: 0.92rem;
      border-radius: 7px;
      border: 1px solid rgba(255,255,255,0.2);
      background: rgba(255,255,255,0.08);
      color: #eee;
      cursor: pointer;
      transition: background 0.15s;
    }
    button:hover { background: rgba(255,255,255,0.18); }
    button.primary {
      background: #c89020;
      color: #fff;
      border-color: #a07010;
      font-weight: bold;
    }
    button.primary:hover { background: #e0a828; }

    #message {
      min-height: 30px;
      font-size: 1.15rem;
      font-weight: bold;
      color: #f0c040;
      text-align: center;
      margin-bottom: 6px;
      text-shadow: 0 1px 4px rgba(0,0,0,0.6);
    }

    #board-scroll { overflow: auto; max-width: 100%; padding-bottom: 8px; }
    #board { position: relative; }

    /* ── 패 스타일 ─────────────────────────────────────── */
    .tile {
      position: absolute;
      width: 56px;
      height: 72px;
      border-radius: 7px;
      background: linear-gradient(160deg, #fffef5 0%, #f0e8d8 100%);
      border: 1.5px solid #c8b890;
      display: flex;
      align-items: center;
      justify-content: center;
      font-size: 30px;
      line-height: 1;
      cursor: pointer;
      user-select: none;
      transition: transform 0.12s, filter 0.12s;
      /* 두께감: 오른쪽·아래 면 */
      box-shadow:
        3px 0   0 0 #b8a070,
        0   4px 0 0 #b8a070,
        3px 4px 0 0 #9a8458,
        4px 6px 10px rgba(0,0,0,0.45);
    }
    .tile.free:hover {
      transform: translateY(-5px);
      filter: brightness(1.07);
    }
    .tile.locked {
      background: linear-gradient(160deg, #c8c0b0 0%, #b0a898 100%);
      border-color: #a09880;
      cursor: default;
      filter: brightness(0.75);
      box-shadow:
        2px 0   0 0 #908070,
        0   3px 0 0 #908070,
        2px 3px 0 0 #786858,
        3px 4px 7px rgba(0,0,0,0.3);
    }
    .tile.selected {
      background: linear-gradient(160deg, #ffe060 0%, #f0b820 100%);
      border-color: #d09000;
      box-shadow:
        3px 0   0 0 #a07000,
        0   4px 0 0 #a07000,
        3px 4px 0 0 #806000,
        0 0 18px 4px rgba(240,180,0,0.6),
        4px 6px 10px rgba(0,0,0,0.4);
    }
    .tile.removed { display: none; }

    /* 패 색상 */
    .man   { color: #bb1100; }
    .pin   { color: #005588; }
    .sou   { color: #006622; }
    .wind  { color: #336699; }
    .dragon{ color: #880044; }

    /* 패 번호 작은 표시 */
    .tile .badge {
      position: absolute;
      top: 3px;
      left: 5px;
      font-size: 10px;
      font-weight: bold;
      opacity: 0.5;
      color: inherit;
    }

    /* 매칭 제거 애니메이션 */
    .tile.anim-remove {
      animation: removeAnim 0.28s ease-out forwards;
    }
    @keyframes removeAnim {
      0%   { transform: scale(1);    opacity: 1; }
      50%  { transform: scale(1.18); opacity: 0.7; }
      100% { transform: scale(0.7);  opacity: 0; }
    }

    /* 힌트 애니메이션 */
    .tile.hint {
      animation: hintAnim 0.5s ease-in-out 4;
    }
    @keyframes hintAnim {
      0%,100% { filter: brightness(1); }
      50%      { filter: brightness(1.5) saturate(1.4); }
    }

    /* 레이어 표시 라벨 */
    #legend {
      margin-top: 14px;
      display: flex;
      gap: 20px;
      font-size: 0.8rem;
      color: #888;
    }
    .leg-item { display: flex; align-items: center; gap: 5px; }
    .leg-box {
      width: 14px; height: 14px; border-radius: 3px;
      border: 1px solid #666;
    }
  </style>
</head>
<body>
  <header>
    <h1>🀄 마작 솔리테어</h1>
    <p>같은 패 2개를 클릭해 제거하세요 (자유로운 패만 선택 가능)</p>
  </header>

  <div id="stats">
    <div class="stat">남은 패: <strong id="remaining">48</strong></div>
    <div class="stat">점수: <strong id="score">0</strong></div>
    <div class="stat">시간: <strong id="timer">0:00</strong></div>
  </div>

  <div id="controls">
    <button class="primary" onclick="newGame()">🔄 새 게임</button>
    <button onclick="showHint()">💡 힌트</button>
    <button onclick="undoLast()">↩ 되돌리기</button>
  </div>

  <div id="message"></div>

  <div id="board-scroll">
    <div id="board"></div>
  </div>

  <div id="legend">
    <div class="leg-item">
      <div class="leg-box" style="background:#fffef5;border-color:#c8b890"></div>선택 가능
    </div>
    <div class="leg-item">
      <div class="leg-box" style="background:#c8c0b0;border-color:#a09880"></div>선택 불가 (잠김)
    </div>
    <div class="leg-item">
      <div class="leg-box" style="background:#ffe060;border-color:#d09000"></div>선택됨
    </div>
  </div>

<script>
// ── 패 종류 (12가지 × 4개 = 48개) ────────────────────────────
const TILE_TYPES = [
  // 만수패
  { id:'m1', emoji:'🀇', cls:'man',  name:'일만' },
  { id:'m2', emoji:'🀈', cls:'man',  name:'이만' },
  { id:'m3', emoji:'🀉', cls:'man',  name:'삼만' },
  { id:'m4', emoji:'🀊', cls:'man',  name:'사만' },
  { id:'m5', emoji:'🀋', cls:'man',  name:'오만' },
  { id:'m6', emoji:'🀌', cls:'man',  name:'육만' },
  { id:'m7', emoji:'🀍', cls:'man',  name:'칠만' },
  { id:'m8', emoji:'🀎', cls:'man',  name:'팔만' },
  { id:'m9', emoji:'🀏', cls:'man',  name:'구만' },
  // 바람패
  { id:'we', emoji:'🀀', cls:'wind', name:'동풍' },
  { id:'ws', emoji:'🀁', cls:'wind', name:'남풍' },
  { id:'ww', emoji:'🀂', cls:'wind', name:'서풍' },
];

// ── 레이아웃: [col, row, layer] ─────────────────────────────
//  layer 0: 8×4 = 32패
//  layer 1: 6×2 (c=1..6, r=1..2) = 12패
//  layer 2: 4×1 (c=2..5, r=1) = 4패  →  합계 48패
const LAYOUT = [];
for (let r = 0; r < 4; r++)
  for (let c = 0; c < 8; c++) LAYOUT.push([c, r, 0]);
for (let r = 1; r <= 2; r++)
  for (let c = 1; c <= 6; c++) LAYOUT.push([c, r, 1]);
for (let c = 2; c <= 5; c++)   LAYOUT.push([c, 1, 2]);

// ── 상수 ─────────────────────────────────────────────────
const TW = 56, TH = 72, GAP = 5, ZX = 7, ZY = 7, PAD = 30;

// ── 상태 ─────────────────────────────────────────────────
let tiles = [];
let selected = null;
let score = 0;
let startTime = null;
let timerHandle = null;
let history = []; // 되돌리기용

// ── 새 게임 ─────────────────────────────────────────────
function newGame() {
  selected = null;
  score = 0;
  history = [];
  setMsg('');
  clearInterval(timerHandle);
  startTime = Date.now();
  timerHandle = setInterval(tickTimer, 500);

  // 덱 만들기: 각 타입 4장
  const deck = [];
  TILE_TYPES.forEach(t => { for (let i = 0; i < 4; i++) deck.push(t.id); });
  shuffle(deck);

  tiles = LAYOUT.map(([c, r, z], i) => ({
    c, r, z,
    typeId: deck[i],
    removed: false,
    el: null,
  }));

  renderBoard();
  updateStats();
}

function shuffle(a) {
  for (let i = a.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [a[i], a[j]] = [a[j], a[i]];
  }
}

// ── 자유 패 여부 ─────────────────────────────────────────
// 위에 패가 없고, 왼쪽 또는 오른쪽 중 하나가 비어야 함
function isFree(tile) {
  if (tile.removed) return false;
  const { c, r, z } = tile;

  // 바로 위 레이어에 같은 위치 패 있으면 잠김
  if (tiles.some(t => !t.removed && t.z === z + 1 && t.c === c && t.r === r))
    return false;

  const leftBlocked  = tiles.some(t => !t.removed && t.z === z && t.c === c - 1 && t.r === r);
  const rightBlocked = tiles.some(t => !t.removed && t.z === z && t.c === c + 1 && t.r === r);
  return !leftBlocked || !rightBlocked;
}

// ── 렌더링 ───────────────────────────────────────────────
function renderBoard() {
  const board = document.getElementById('board');
  board.innerHTML = '';

  const bw = 8 * (TW + GAP) + 3 * ZX + PAD * 2;
  const bh = 4 * (TH + GAP) + 3 * ZY + PAD * 2 + 10;
  board.style.width  = bw + 'px';
  board.style.height = bh + 'px';

  for (const tile of tiles) {
    const info = TILE_TYPES.find(t => t.id === tile.typeId);
    const px = tile.c * (TW + GAP) + tile.z * ZX + PAD;
    const py = tile.r * (TH + GAP) - tile.z * ZY + PAD;
    const zi = tile.z * 500 + (8 - tile.r) * 10 + tile.c;

    const el = document.createElement('div');
    el.className = `tile ${info.cls}`;
    el.style.cssText = `left:${px}px; top:${py}px; z-index:${zi}`;

    // 패 내용
    const badge = document.createElement('span');
    badge.className = 'badge';
    badge.textContent = info.name;
    el.appendChild(badge);

    const face = document.createElement('span');
    face.textContent = info.emoji;
    el.appendChild(face);

    el.addEventListener('click', () => onTileClick(tile));
    tile.el = el;
    board.appendChild(el);
  }

  refreshFreeState();
}

function refreshFreeState() {
  for (const t of tiles) {
    if (!t.el || t.removed) continue;
    const free = isFree(t);
    t.el.classList.toggle('free', free);
    t.el.classList.toggle('locked', !free);
    if (t === selected && !free) {
      t.el.classList.remove('selected');
      selected = null;
    }
  }
}

// ── 클릭 처리 ────────────────────────────────────────────
function onTileClick(tile) {
  if (tile.removed || !isFree(tile)) return;

  // 같은 패 다시 클릭 → 선택 해제
  if (tile === selected) {
    tile.el.classList.remove('selected');
    selected = null;
    return;
  }

  if (selected && selected.typeId === tile.typeId) {
    // ✅ 매칭 성공
    const a = selected, b = tile;
    a.el.classList.remove('selected');
    a.el.classList.add('anim-remove');
    b.el.classList.add('anim-remove');
    history.push([a, b]);
    selected = null;

    setTimeout(() => {
      a.removed = true;  a.el.classList.add('removed');
      b.removed = true;  b.el.classList.add('removed');
      score += 10;
      refreshFreeState();
      updateStats();
      checkEnd();
    }, 270);

  } else {
    // 다른 패 선택
    if (selected) selected.el.classList.remove('selected');
    selected = tile;
    tile.el.classList.add('selected');
  }
}

// ── 힌트 ────────────────────────────────────────────────
function showHint() {
  const free = tiles.filter(t => !t.removed && isFree(t));
  for (let i = 0; i < free.length; i++) {
    for (let j = i + 1; j < free.length; j++) {
      if (free[i].typeId === free[j].typeId) {
        [free[i], free[j]].forEach(t => {
          t.el.classList.remove('hint');
          void t.el.offsetWidth; // reflow
          t.el.classList.add('hint');
          setTimeout(() => t.el.classList.remove('hint'), 2100);
        });
        score = Math.max(0, score - 3);
        updateStats();
        return;
      }
    }
  }
  setMsg('😶 가능한 힌트가 없습니다.');
}

// ── 되돌리기 ─────────────────────────────────────────────
function undoLast() {
  if (!history.length) return;
  const [a, b] = history.pop();
  a.removed = false; a.el.classList.remove('removed', 'anim-remove');
  b.removed = false; b.el.classList.remove('removed', 'anim-remove');
  score = Math.max(0, score - 10);
  setMsg('');
  refreshFreeState();
  updateStats();
}

// ── 종료 체크 ────────────────────────────────────────────
function checkEnd() {
  const left = tiles.filter(t => !t.removed);
  if (left.length === 0) {
    clearInterval(timerHandle);
    setMsg(`🎉 완전 클리어! 최종 점수: ${score}점`);
    return;
  }

  const free = left.filter(t => isFree(t));
  let possible = false;
  outer: for (let i = 0; i < free.length; i++)
    for (let j = i + 1; j < free.length; j++)
      if (free[i].typeId === free[j].typeId) { possible = true; break outer; }

  if (!possible) {
    clearInterval(timerHandle);
    setMsg('😢 더 이상 맞출 패가 없습니다. 새 게임을 시작하세요!');
  }
}

// ── UI 업데이트 ───────────────────────────────────────────
function updateStats() {
  document.getElementById('remaining').textContent = tiles.filter(t => !t.removed).length;
  document.getElementById('score').textContent = score;
}

function tickTimer() {
  if (!startTime) return;
  const s = Math.floor((Date.now() - startTime) / 1000);
  const mm = Math.floor(s / 60), ss = s % 60;
  document.getElementById('timer').textContent = `${mm}:${String(ss).padStart(2,'0')}`;
}

function setMsg(text) {
  document.getElementById('message').textContent = text;
}

// ── 시작 ─────────────────────────────────────────────────
newGame();
</script>
</body>
</html>

결과와 배운 점

실습을 진행해서 아직 뭔가 더 향상된 버전을 만들어 보진 못 했지만 영상 다시보기하면서 도전해 보면 좋겠다 싶습니다.

앞으로의 계획이 있다면 들려주세요.

skill을 한 번 써보면 좋겠다는 생각을 해 봅니다.

뉴스레터 무료 구독

👉 이 게시글도 읽어보세요