大富豪

革命状態: なし
-
あなたの手札:
📖 ゲームルール(クリックで開く)
  • 1人プレイ(あなた vs AI)
  • カードの強さ: 3 < ... < A < 2(革命時は逆転)
  • 出せるカード: 前のカードより強いカード
  • ペアや階段出し: 同じランク / 同じスートで連番(3枚以上)
  • 8切り: 8を出すと場が強制流れ
  • 革命: 同じランクのカードを4枚出すと強さが逆転
  • パスが2回続くと: 場が流れて最後に出した人から再開
view source

JavaScript

const suits = ['♠', '♥', '♣', '♦'];
const ranks = [3, 4, 5, 6, 7, 8, 9, 10, 'J', 'Q', 'K', 'A', 2];

let players = [];
let currentPlayerIndex = 0;
let topCards = [];
let passCount = 0;
let lastPlayerIndex = -1;
let isReversed = false;
let selected = new Set();


function createDeck() {
  const deck = [];
  for (const suit of suits) {
    for (const rank of ranks) {
      deck.push({ suit, rank });
    }
  }
  return deck.sort(() => Math.random() - 0.5);
}

function compareCards(a, b) {
  const indexA = ranks.indexOf(a.rank);
  const indexB = ranks.indexOf(b.rank);
  return isReversed ? indexB - indexA : indexA - indexB;
}

function isStraight(cards) {
  if (cards.length < 3) return false;
  const sorted = [...cards].sort(compareCards);
  const suit = sorted[0].suit;
  for (let i = 1; i < sorted.length; i++) {
    if (sorted[i].suit !== suit) return false;
    if (ranks.indexOf(sorted[i].rank) !== ranks.indexOf(sorted[i - 1].rank) + 1) {
      return false;
    }
  }
  return true;
}

function isPlayableSet(cards, topCards) {
  if (cards.length === 0) return false;
  const isStraightPlay = isStraight(cards);
  const isGroupPlay = cards.every(c => c.rank === cards[0].rank);

  if (!isStraightPlay && !isGroupPlay && cards.length > 1) return false;
  if (!topCards.length) return true;

  const isTopStraight = isStraight(topCards);
  const isTopGroup = topCards.every(c => c.rank === topCards[0].rank);

  if (isStraightPlay && isTopStraight) {
    if (cards.length !== topCards.length) return false;
    return compareCards(cards[0], topCards[0]) > 0;
  }

  if (isGroupPlay && isTopGroup) {
    if (cards.length !== topCards.length) return false;
    return compareCards(cards[0], topCards[0]) > 0;
  }

  return false;
}

function isEightCut(cards) {
  return cards.some(card => card.rank === 8);
}

function startGame() {
  const deck = createDeck();
  players = [
    { name: 'あなた', hand: deck.slice(0, 13), isAI: false },
    { name: 'AI A', hand: deck.slice(13, 26), isAI: true },
    { name: 'AI B', hand: deck.slice(26, 39), isAI: true }
  ];
  topCards = [];
  passCount = 0;
  isReversed = false;
  currentPlayerIndex = 0;
  lastPlayerIndex = -1;
  initialRenderPlayerHand();
  updatePlayerHandState();
  updateCardInteractivity();
  render();
  maybeRunAITurn();
}

function clearFieldAndResetTurn() {
  topCards = [];
  log("全員がパスしたため場が流れました。", 'neutral');
  passCount = 0;
  currentPlayerIndex = lastPlayerIndex;

  renderTopCards(); // 場を消す
  updatePlayerHandState(); // ← 明るく or 暗く切り替え
  updateCardInteractivity(); // ← 出せるカードだけ有効に

  maybeRunAITurn();
}


function nextTurn() {
  currentPlayerIndex = (currentPlayerIndex + 1) % players.length;
  if (currentPlayerIndex === lastPlayerIndex) {
    clearFieldAndResetTurn();
    return;
  }
  updatePlayerHandState();
  updateCardInteractivity();
  maybeRunAITurn();
  render();
}

function maybeRunAITurn() {
  const currentPlayer = players[currentPlayerIndex];
  if (currentPlayer.isAI) {
    setTimeout(() => aiTurn(currentPlayer), 1000);
  }
}

function aiTurn(player) {
  const playableSets = findPlayableSets(player.hand);

  if (playableSets.length) {
    const chosenSet = playableSets[0];
    topCards = chosenSet;
    lastPlayerIndex = currentPlayerIndex;
    passCount = 0;

    for (const card of chosenSet) {
      const idx = player.hand.findIndex(c => c.suit === card.suit && c.rank === card.rank);
      player.hand.splice(idx, 1);
    }

    log(`${player.name} は ${topCards.map(c => `${c.suit}${c.rank}`).join(' ')} を出しました。`, 'ai');
    checkWin(player);
  } else {
    log(`${player.name} はパスしました。`, 'ai');
    passCount++;

    if (currentPlayerIndex === lastPlayerIndex) {
      clearFieldAndResetTurn();
      return;
    }
  }

  nextTurn();
}

function findPlayableSets(hand) {
  const sets = [];

  const rankGroups = {};
  for (let card of hand) {
    const r = card.rank;
    if (!rankGroups[r]) rankGroups[r] = [];
    rankGroups[r].push(card);
  }

  for (const rank in rankGroups) {
    const group = rankGroups[rank];
    for (let count = Math.min(group.length, 4); count >= 1; count--) {
      const set = group.slice(0, count);
      if (isPlayableSet(set, topCards)) {
        sets.push(set);
        break;
      }
    }
  }

  const suitGroups = {};
  for (let card of hand) {
    if (!suitGroups[card.suit]) suitGroups[card.suit] = [];
    suitGroups[card.suit].push(card);
  }

  for (const suit in suitGroups) {
    const cards = suitGroups[suit].sort(compareCards);
    for (let i = 0; i <= cards.length - 3; i++) {
      for (let len = 3; len <= cards.length - i; len++) {
        const slice = cards.slice(i, i + len);
        if (isStraight(slice) && isPlayableSet(slice, topCards)) {
          sets.push(slice);
          break;
        }
      }
    }
  }

  return sets;
}


function updateCardInteractivity() {
  const currentPlayer = players[currentPlayerIndex];
  const container = document.getElementById('playerHand');

  if (currentPlayer.isAI) return; // AIの手番ならスキップ

  const cardDivs = container.querySelectorAll('.card');

  // 🔽 場が流れている(カードなし)なら、全カード有効にする
  if (topCards.length === 0) {
    cardDivs.forEach(div => div.classList.remove('disabled'));
    return;
  }

  cardDivs.forEach(div => {
    const id = div.dataset.id;
    const [suit, rankStr] = id.split('-');
    const rank = isNaN(rankStr) ? rankStr : parseInt(rankStr);

    const card = { suit, rank };

    const canPlay = isPlayableSet([card], topCards);

    if (canPlay) {
      div.classList.remove('disabled');
    } else {
      div.classList.add('disabled');
    }
  });
}

function passTurn() {
  const player = players[currentPlayerIndex];
  if (player.isAI) return;

  // 🔽 選択状態をリセット
  selected.clear();
  document.querySelectorAll('#playerHand .card').forEach(div => {
    div.style.background = ''; // ハイライト解除
  });

  log("あなたはパスしました。", 'player');
  passCount++;

  if (currentPlayerIndex === lastPlayerIndex) {
    clearFieldAndResetTurn();
    return;
  }

  nextTurn();
}

function checkWin(player) {
  if (player.hand.length === 0) {
    log(`🎉 ${player.name}の勝ち!`, player.isAI ? 'ai' : 'player');
  }
}


function renderOpponents() {
  const container = document.getElementById('opponentsArea');
  container.innerHTML = '';

  players.forEach((player, index) => {
    if (player.isAI) {
      const div = document.createElement('div');
      div.className = 'opponent';

      const title = document.createElement('strong');
      title.textContent = `${player.name} の残りカード: `;
      div.appendChild(title);

      const count = document.createElement('span');
      count.textContent = player.hand.length;
      div.appendChild(count);

      const handLabel = document.createElement('strong');
      //handLabel.textContent = '(手札表示:)';
      //div.appendChild(document.createElement('br'));
      div.appendChild(handLabel);

      const handDiv = document.createElement('div');
      handDiv.className = 'opponentHand';
      player.hand.forEach(() => {
        const backCard = document.createElement('div');
        backCard.className = 'card back';
        handDiv.appendChild(backCard);
      });
      div.appendChild(handDiv);

      container.appendChild(div);
    }
  });
}


function render() {
  renderPlayerHand();
  renderOpponents();
  renderTopCards();
  document.getElementById('revState').textContent = isReversed ? '革命中🔥' : 'なし';
}

function initialRenderPlayerHand() {

  const container = document.getElementById('playerHand');
  container.innerHTML = ''; // 初回のみ

  // カード表示用の枠
  const cardsContainer = document.createElement('div');
  cardsContainer.className = 'cards';

  // ボタン表示用の枠
  const actionsContainer = document.createElement('div');
  actionsContainer.className = 'actions';

  // containerに追加
  container.appendChild(cardsContainer);
  container.appendChild(actionsContainer);

  const currentPlayer = players[currentPlayerIndex];
  if (currentPlayer.isAI) return;

  currentPlayer.hand.sort(compareCards);

  currentPlayer.hand.forEach((card) => {
    const cardId = `${card.suit}-${card.rank}`;
    const div = document.createElement('div');
    div.className = 'card';
    div.dataset.id = cardId;

    const isRed = card.suit === '♥' || card.suit === '♦';
    div.classList.add(isRed ? 'red' : 'black');

    const rankSpan = document.createElement('div');
    rankSpan.className = 'rank';
    rankSpan.textContent = card.rank;

    const suitSpan = document.createElement('div');
    suitSpan.className = 'suit';
    suitSpan.textContent = card.suit;

    div.appendChild(rankSpan);
    div.appendChild(suitSpan);

    div.onclick = () => {
      if (selected.has(cardId)) {
        selected.delete(cardId);
        div.style.background = '';
      } else {
        selected.add(cardId);
        div.style.background = '#ddd';
      }
    };

    cardsContainer.appendChild(div); //
  });


  // ボタン類は固定なので一回描画でOK
  createActionButtons(actionsContainer);
}

function createActionButtons(container) {
  const playBtn = document.createElement('button');
  playBtn.textContent = "選んだカードを出す";
  playBtn.onclick = () => {
    const currentPlayer = players[currentPlayerIndex];
    const selectedCards = currentPlayer.hand.filter(card =>
      selected.has(`${card.suit}-${card.rank}`)
    );

    if (!isPlayableSet(selectedCards, topCards)) {
      log("❌ その出し方はできません!", 'player');
      return;
    }

    topCards = selectedCards;
    lastPlayerIndex = currentPlayerIndex;
    passCount = 0;

    // 1. UI からカード要素だけ削除
    selectedCards.forEach(card => {
      const cardId = `${card.suit}-${card.rank}`;
      const cardDiv = document.querySelector(`.card[data-id="${cardId}"]`);
      if (cardDiv) cardDiv.remove();
    });

    // 2. 手札から削除
    selectedCards.forEach(card => {
      const idx = currentPlayer.hand.findIndex(c =>
        c.suit === card.suit && c.rank === card.rank
      );
      if (idx !== -1) currentPlayer.hand.splice(idx, 1);
    });

    selected.clear();

    log(`あなたは ${topCards.map(c => `${c.suit}${c.rank}`).join(' ')} を出しました。`, 'player');
    checkWin(currentPlayer);

    renderTopCards(); // ← これは場のカードの描画用関数
    nextTurn();
  };

  const passBtn = document.createElement('button');
  passBtn.textContent = "パスする";
  passBtn.onclick = passTurn;

  container.appendChild(playBtn);
  container.appendChild(passBtn);
}


function updatePlayerHandState() {
  const container = document.getElementById('playerHand');
  const currentPlayer = players[currentPlayerIndex];

  if (currentPlayer.isAI) {
    container.classList.remove('active');
    container.classList.add('inactive');
  } else {
    container.classList.remove('inactive');
    container.classList.add('active');
  }
}
function renderTopCards() {
  const container = document.getElementById('topCardArea');
  container.innerHTML = '';

  if (topCards.length === 0) {
    container.textContent = 'なし';
    return;
  }

  topCards.forEach(card => {
    const div = document.createElement('div');
    div.className = 'card';
    div.dataset.id = `${card.suit}-${card.rank}`;

    const isRed = card.suit === '♥' || card.suit === '♦';
    div.classList.add(isRed ? 'red' : 'black');

    const rankSpan = document.createElement('div');
    rankSpan.className = 'rank';
    rankSpan.textContent = card.rank;

    const suitSpan = document.createElement('div');
    suitSpan.className = 'suit';
    suitSpan.textContent = card.suit;

    div.appendChild(rankSpan);
    div.appendChild(suitSpan);

    container.appendChild(div);
  });
}

function renderPlayerHand() {
  // 既に手札描画が済んでる前提(DOMは壊さない)

  const currentPlayer = players[currentPlayerIndex];
  const container = document.getElementById('playerHand');

  if (currentPlayer.isAI) {
    container.classList.remove('active');
    container.classList.add('inactive');
    return;
  } else {
    container.classList.remove('inactive');
    container.classList.add('active');
  }

  updateCardInteractivity(); // ← ここで使えるカードを更新!
}



function log(message, sender = 'neutral') {
  const logDiv = document.getElementById('log');
  const p = document.createElement('p');
  p.textContent = message;
  p.classList.add('log-entry');

  if (sender === 'player') p.classList.add('log-player');
  else if (sender === 'ai') p.classList.add('log-ai');
  else p.classList.add('log-neutral');

  logDiv.appendChild(p);
  logDiv.scrollTop = logDiv.scrollHeight;
}

// ゲーム開始!
startGame();

CSS

body {
  font-family: sans-serif;
  padding: 20px;
  background: #f7f7f7;
}

button{
}
.card {
  display: inline-block;
  width: 60px;
  height: 90px;
  border: 1px solid #aaa;
  border-radius: 8px;
  margin: 5px;
  background: white;
  box-shadow: 2px 2px 5px rgba(0,0,0,0.1);
  position: relative;
  font-weight: bold;
  font-size: 18px;
  text-align: center;
  cursor: pointer;
  transition: transform 0.2s;
}

#topCardArea{
  height:100px;
  padding:10px;
}


#playerHand.inactive {
  opacity: 0.4;
  pointer-events: none;
}

#playerHand.active {
  opacity: 1;
  pointer-events: auto;
}

.card:hover {
  background: #f0f0f0;
  transform: scale(1.05);
}

.card .rank {
  position: absolute;
  top: 5px;
  left: 5px;
  font-size: 16px;
}

.card .suit {
  position: absolute;
  bottom: 5px;
  right: 5px;
  font-size: 20px;
}

.card.red {
  color: red;
}

.card.black {
  color: black;
}

#playerHand .card.disabled {
  opacity: 0.3;
  pointer-events: none;
  filter: grayscale(60%);
}

.hand, .opponent {
  margin-bottom: 4px;
}

button {
  margin-top: 10px;
  padding: 6px 12px;
  font-size: 16px;
}


.card.back {
  background: repeating-linear-gradient(45deg, #666, #666 5px, #999 5px, #999 10px);
  color: transparent;
  position: relative;
}

.card.back::after {
  content: "🂠"; /* カード裏マーク(開発用) */
  color: white;
  font-size: 24px;
  position: absolute;
  top: 30%;
  left: 20%;
}
#log {
  height: 80px;
  overflow-y: auto;
  padding: 4px;
  border: 1px solid #ccc;
  background: #fff;
}

.log-entry {
  margin: 5px 0;
  font-weight: bold;
}

.log-player {
  color: blue;
}

.log-ai {
  color: crimson;
}

.log-neutral {
  color: black;
}

HTML

ページのソースを表示 : Ctrl+U , DevTools : F12

view-source:https://hi0a.com/demo/-js/js-game-daifugo/

ABOUT

hi0a.com 「ひまアプリ」は無料で遊べるミニゲームや便利ツールを公開しています。

プログラミング言語の動作デモやWEBデザイン、ソースコード、フロントエンド等の開発者のための技術を公開しています。

必要な機能の関数をコピペ利用したり勉強に活用できます。

プログラムの動作サンプル結果は画面上部に出力表示されています。

環境:最新のブラウザ GoogleChrome / Windows / Android / iPhone 等の端末で動作確認しています。

画像素材や音素材は半分自作でフリー素材配布サイトも利用しています。LINK参照。

動く便利なものが好きなだけで技術自体に興味はないのでコードは汚いです。

途中放置や実験状態、仕様変更、API廃止等で動かないページもあります。