꿈을 바구니에 담아 간직하다 보면!!

지금 참 힘들죠? 근데 내일은 지금보다 덜 힘들거예요

힘든 건 오늘만이 아니다. 내일도, 그리고 그 다음 날도 계속될 것이다.

PHP Tip

릴 게임 · Lucky Reels source

duaidot 2025. 12. 19. 12:00

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>HTML5 릴게임</title>
<style>
  :root {
    --bg:#0e0f13; --panel:#1b1e25; --accent:#ffcc00; --text:#e9edf1; --muted:#9aa3ad;
  }
  body {
    margin:0; min-height:100vh; display:flex; align-items:center; justify-content:center;
    background:radial-gradient(1200px 600px at 50% 30%, #151822 0%, #0d0f14 60%, #07080b 100%);
    font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
    color:var(--text);
  }
  .game {
    width:min(520px, 92vw); background:linear-gradient(#21242c, #191c23);
    border:1px solid #2b2f39; border-radius:18px; box-shadow:0 18px 40px rgba(0,0,0,.5);
    overflow:hidden;
  }
  .header {
    padding:16px 20px; display:flex; justify-content:space-between; align-items:center;
    background:#14171d;
    border-bottom:1px solid #232733;
  }
  .title { font-weight:700; letter-spacing:.4px; }
  .balance { font-size:14px; color:var(--muted); }
  .reels {
    display:grid; grid-template-columns:repeat(3, 1fr); gap:14px; padding:20px;
  }
  .reel {
    height:160px; background:linear-gradient(#111318, #0c0e12); border:1px solid #2a2e39;
    border-radius:12px; position:relative; overflow:hidden;
    box-shadow: inset 0 0 0 1px #1f2330, inset 0 8px 18px rgba(0,0,0,.35);
  }
  .strip {
    position:absolute; left:0; right:0; top:0;
    display:flex; flex-direction:column; align-items:center; gap:10px; padding:10px 0;
    transition: transform cubic-bezier(.2,.8,.2,1);
  }
  .symbol {
    width:100%; height:70px; display:flex; align-items:center; justify-content:center;
    font-size:42px; filter: drop-shadow(0 2px 1px rgba(0,0,0,.35));
    user-select:none;
  }
  .controls {
    padding:14px 20px; display:flex; gap:10px; align-items:center; border-top:1px solid #232733;
    background:#14171d;
  }
  button {
    flex:1; padding:12px 14px; border:none; border-radius:10px; font-weight:700;
    background:linear-gradient(180deg, #ffd54d, #ffb300); color:#241a00;
    box-shadow:0 8px 16px rgba(255, 179, 0, .25); cursor:pointer;
  }
  button:disabled {
    background:#3c3f49; color:#9aa3ad; box-shadow:none; cursor:not-allowed;
  }
  .bet {
    display:flex; align-items:center; gap:8px;
    padding:10px 12px; border-radius:10px; background:#1b1f2a; color:var(--muted);
  }
  .msg {
    padding:10px 20px; font-weight:600; min-height:28px; color:#9dd7ff;
  }
  .win {
    animation: flash 900ms ease-in-out 2;
  }
  @keyframes flash {
    0% { color:#9dd7ff; }
    50% { color:#79ff9d; text-shadow:0 0 12px rgba(121,255,157,.65); }
    100% { color:#9dd7ff; }
  }
</style>
</head>
<body>
  <div class="game">
    <div class="header">
      <div class="title">릴 게임 · Lucky Reels</div>
      <div class="balance">잔액: <span id="balance">1000</span> 코인</div>
    </div>

    <div class="reels" id="reels">
      <div class="reel"><div class="strip"></div></div>
      <div class="reel"><div class="strip"></div></div>
      <div class="reel"><div class="strip"></div></div>
    </div>

    <div class="msg" id="msg"></div>

    <div class="controls">
      <div class="bet">베팅: <strong id="bet">50</strong> 코인</div>
      <button id="spinBtn">SPIN</button>
    </div>
  </div>

<script>
(() => {
  // 심볼 목록 (이모지라서 별도 이미지 없이 바로 사용)
  const SYMBOLS = ['🍒','🍋','🍀','⭐','🔔','7️⃣','💎'];
  const reels = [...document.querySelectorAll('.strip')];
  const spinBtn = document.getElementById('spinBtn');
  const msg = document.getElementById('msg');
  const balanceEl = document.getElementById('balance');
  const betEl = document.getElementById('bet');

  let balance = 1000;
  let bet = 50;
  let spinning = false;

  // 초기 스트립 구성
  function buildStrip(strip) {
    strip.innerHTML = '';
    // 가시성 확보를 위해 20칸 정도 구성
    const pool = [];
    for (let i = 0; i < 20; i++) {
      const sym = SYMBOLS[Math.floor(Math.random() * SYMBOLS.length)];
      pool.push(sym);
    }
    pool.forEach(s => {
      const el = document.createElement('div');
      el.className = 'symbol';
      el.textContent = s;
      strip.appendChild(el);
    });
    // 시작 위치 랜덤
    const start = -(Math.floor(Math.random() * (pool.length - 3)) * 80);
    strip.style.transform = `translateY(${start}px)`;
  }

  reels.forEach(buildStrip);

  function setMessage(text, winning=false) {
    msg.textContent = text;
    msg.classList.toggle('win', winning);
  }

  function updateBalance(diff) {
    balance += diff;
    balanceEl.textContent = balance;
    if (balance <= 0) {
      spinBtn.disabled = true;
      setMessage('잔액이 없습니다. 새로고침으로 초기화하세요.');
    }
  }

  // 스핀 로직
  async function spin() {
    if (spinning) return;
    if (balance < bet) { setMessage('잔액이 부족합니다. 베팅을 줄이거나 초기화하세요.'); return; }
    spinning = true;
    spinBtn.disabled = true;
    setMessage('회전 중...');

    updateBalance(-bet);

    // 각 릴에 서로 다른 지속시간과 감속 적용
    const durations = [900, 1100, 1300];
    const results = [];

    await Promise.all(reels.map((strip, i) => new Promise(resolve => {
      const total = strip.children.length;
      // 목표 인덱스(중앙에 멈출 심볼)
      const targetIndex = Math.floor(Math.random() * (total - 2)) + 1; // 1~(total-2)
      const targetY = -(targetIndex * 80 - 40); // 중앙 보정
      strip.style.transitionDuration = durations[i] + 'ms';
      strip.style.transform = `translateY(${targetY}px)`;
      strip.addEventListener('transitionend', function handler() {
        strip.removeEventListener('transitionend', handler);
        // 중앙, 윗, 아래 심볼 추출 (중앙 기준)
        const center = strip.children[targetIndex].textContent;
        results[i] = center;
        resolve();
      }, { once:true });
    })));

    // 결과 판정
    const [a,b,c] = results;
    let payout = 0;
    let text = `결과: ${a} | ${b} | ${c}`;

    const allSame = (a === b && b === c);
    const twoSame = (a === b || b === c || a === c);

    if (allSame) {
      // 심볼 별 배당
      const multiplier = ({
        '🍒': 4, '🍋': 4, '🍀': 6, '⭐': 8, '🔔': 10, '7️⃣': 20, '💎': 25
      })[a] || 4;
      payout = bet * multiplier;
      updateBalance(payout);
      setMessage(`${text} · 대박! 배당 x${payout / bet} → +${payout} 코인`, true);
    } else if (twoSame) {
      payout = Math.floor(bet * 1.5);
      updateBalance(payout);
      setMessage(`${text} · 2개 일치! +${payout} 코인`, true);
    } else {
      setMessage(`${text} · 꽝! -${bet} 코인`);
    }

    spinning = false;
    spinBtn.disabled = balance <= 0;
  }

  spinBtn.addEventListener('click', spin);
  // 모바일 더블탭 방지, 터치도 클릭과 동일 처리
  spinBtn.addEventListener('touchend', (e) => { e.preventDefault(); spin(); }, { passive:false });

  // 간단한 베팅 조절: 버튼 길게 누르면 베팅 증가/감소 아이디어 (옵션)
  // 키보드 지원
  window.addEventListener('keydown', (e) => {
    if (e.key === ' ' || e.key.toLowerCase() === 's') spin();
    if (e.key === 'ArrowUp') { bet = Math.min(bet + 10, 200); betEl.textContent = bet; }
    if (e.key === 'ArrowDown') { bet = Math.max(bet - 10, 10); betEl.textContent = bet; }
  });

  // 처음 안내
  setMessage('SPIN 버튼 또는 스페이스바로 시작하세요. 화살표로 베팅 조절.');
})();
</script>
</body>
</html>

소스 실행화면