【ゲーム】JavaScript:12 七並べ

 「🃏 七並べ 🃏」は、トランプ1デッキ(4種×13枚)を4人で配り、ダイヤの7から始めて各スートごとに順に1~6、8~13 を場に出していくカード配置ゲームです。最後まで手札を使い切るか、他プレイヤーが残り手札を失格したときが勝敗の決着となります。

遊び方と操作方法

  • 初期化ボタンまたはページ読み込み時にゲームが開始し、全員に13枚ずつ配牌。
  • ダイヤ7を持つプレイヤーが最初に自動で7を場に出し手札から除去。ダイヤ7を持っていたプレイヤーが最初に場にカードを出す権利が与えられます。
  • 順番に自分の番が来たら、自分の手札の中から場に出せるカード(現在場のそのスートの最小値の1つ前、または最大値の1つ後)をクリックして出します。
  • 出せるカードがない場合は「🛑 パス」を押し、最大3回までパス可能。4回目を超えると失格となり以後手番をパスします。
  • 全員が出し切るか、残り生存プレイヤーが1名になると勝利。

ルール

  • 場は各スートごとに連続した数値の範囲(初期は7のみ)。スートごとに最小値と最大値を管理。
  • カードは「スート7」から出発し、以降は min-1 または max+1 の数値のみ合法手。
  • パスは各プレイヤー3回まで有効。4回目以降は失格となりゲームから除外。
  • 最初に手札を使い切ったプレイヤー、または最後まで生存したプレイヤーが勝利。

🎮ゲームプレイ

以下のリンク先から実際にプレイできます。

12 七並べ

素材のダウンロード

以下のリンクから使用する素材をダウンロードできます。

sevens_bg.png

ゲーム画面イメージ

プログラム全文(sevens.html)

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>🃏 七並べ 🃏</title>
  <style>
    /* 全体スタイル */
    body {
      margin: 0;
      padding: 0;
      font-family: 'Arial', sans-serif;
      text-align: center;
      /* 背景画像 */
      background: url('sevens_bg.png') no-repeat center center fixed;
      background-size: cover;
      color: white;
    }
    /* タイトル・見出し */
    h1, h2 {
      display: inline-block;
      background: rgba(0,0,0,0.6);
      padding: 10px 20px;
      border-radius: 8px;
      margin: 20px 0;
      text-shadow: 2px 2px 4px rgba(0,0,0,0.7);
    }
    h1 { font-size: 2.5em; }
    /* 「場」の見出しを大きく */
    #fieldTitle { font-size: 2.2em; }
    h2:not(#fieldTitle) { font-size: 1.8em; }

    /* フィールドのテーブル */
    #field {
      margin: 10px auto;
      border-collapse: collapse;
      background: rgba(0,0,0,0.6);
    }
    #field td, #field th {
      width: 40px;
      height: 30px;
      border: 1px solid #ccc;
      color: white;
      text-align: center;
      font-size: 0.9em;
    }
    #field th {
      background: rgba(255,255,255,0.2);
    }

    /* 手札 */
    .cards {
      display: flex;
      justify-content: center;
      flex-wrap: wrap;
      margin: 10px;
    }
    .card {
      width: 60px;
      height: 90px;
      margin: 5px;
      border-radius: 6px;
      background-color: white;
      color: black;
      font-size: 16px;
      line-height: 90px;
      text-align: center;
      font-weight: bold;
      box-shadow: 0 4px 6px rgba(0,0,0,0.3);
      cursor: pointer;
      transition: transform 0.2s;
    }
    .card.red { color: red; }
    .card.disabled {
      opacity: 0.4;
      cursor: not-allowed;
      transform: none;
    }
    .card:hover:not(.disabled) {
      transform: scale(1.1);
    }

    /* ボタン */
    button {
      padding: 8px 16px;
      margin: 5px;
      font-size: 14px;
      font-weight: bold;
      color: white;
      background-color: #ff5733;
      border: none;
      border-radius: 4px;
      cursor: pointer;
      box-shadow: 0 4px 6px rgba(0,0,0,0.3);
      transition: background-color 0.2s, transform 0.2s;
    }
    button:hover:not(:disabled) {
      background-color: #ff2e00;
      transform: scale(1.05);
    }
    button:disabled {
      background-color: grey !important;
      cursor: not-allowed;
      opacity: 0.6;
      transform: none !important;
    }
    #compCounts {
      display: inline-block;
      background: rgba(0,0,0,0.8);
      padding: 12px 16px;
      border-radius: 6px;
      font-size: 1.5em;
      text-shadow: 2px 2px 4px rgba(0,0,0,0.7);
      color: #ffd700;
      margin: 10px 0;
    }

    /* メッセージ */
    #message {
      display: inline-block;
      margin: 20px;
      padding: 10px 20px;
      background: rgba(0,0,0,0.6);
      border-radius: 5px;
      font-size: 1.2em;
      text-shadow: 2px 2px 4px rgba(0,0,0,0.7);
      min-width: 200px;
    }
  </style>
</head>
<body>
  <!-- タイトル -->
  <h1>🃏 七並べ 🃏</h1>

  <!-- フィールド -->
  <div>
    <h2 id="fieldTitle">🗺️ 場 🗺️</h2><br>
    <table id="field"></table>
  </div>

  <!-- プレイヤー手札 -->
  <div>
    <h2>🧑 あなたの手札 🧑</h2>
    <div class="cards" id="playerHand"></div>
    <button id="passBtn" onclick="playerPass()">🛑 パス</button>
  </div>

  <!-- コンピュータ手札枚数 -->
  <div>
    <h2>💻 コンピュータの手札 💻</h2>
    <p>
      <div id="compCounts">
        コンピュータ1: <span id="comp0Count">13</span>枚 
        コンピュータ2: <span id="comp1Count">13</span>枚 
        コンピュータ3: <span id="comp2Count">13</span>枚
      </div> 
    </p>
  </div>

  <!-- メッセージ -->
  <div id="message">ゲーム初期化中...</div>

  <!-- もう一度ボタン -->
  <button id="restartBtn" onclick="initGame()" style="display:none;">🔄 もう一度</button>

  <script>
    // 定数・変数
    const suits = ['♠','♥','♦','♣'];
    const maxPass = 3;
    const numPlayers = 4;
    const players = [];  // { hand:[], pass:0, eliminated:false }
    let deck = [];
    const fieldState = {}; // suit -> {min, max}
    let currentPlayer = 0;
    let gameOver = false;

    // プレイヤー初期化
    for (let i = 0; i < numPlayers; i++) {
      players.push({ hand: [], pass: 0, eliminated: false });
    }

    // ゲーム開始
    window.onload = initGame;

    function initGame() {
      gameOver = false;
      document.getElementById('restartBtn').style.display = 'none';
      // フィールド初期化
      for (let s of suits) fieldState[s] = { min: 7, max: 7 };

      // デッキ作成・シャッフル
      deck = createDeck();
      shuffle(deck);

      // 配布
      for (let i = 0; i < numPlayers; i++) {
        players[i].hand = deck.splice(0, 13);
        players[i].pass = 0;
        players[i].eliminated = false;
      }

      // 開始プレイヤー判定(ダイヤ7を持っていた人)
      for (let i = 0; i < numPlayers; i++) {
        if (players[i].hand.some(c => c.suit === '♦' && c.value === 7)) {
          currentPlayer = i;
          break;
        }
      }

      // 全7を場に出して手札から除去
      for (let i = 0; i < numPlayers; i++) {
        players[i].hand = players[i].hand.filter(c => c.value !== 7);
      }

      // 描画
      renderField();
      renderHands();
      updateCompCounts();
      updateUI();
      showMessage(`ゲーム開始! ${currentPlayer === 0 ? 'あなた' : 'コンピュータ'+currentPlayer} の番です`);
      // AIの場合、自動で次のターン開始
      if (currentPlayer !== 0) setTimeout(computerTurn, 800);
    }

    // デッキ生成
    function createDeck() {
      const d = [];
      for (let s of suits) {
        for (let v = 1; v <= 13; v++) {
          d.push({ suit: s, value: v });
        }
      }
      return d;
    }
    // シャッフル
    function shuffle(d) {
      for (let i = d.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        [d[i], d[j]] = [d[j], d[i]];
      }
    }

    // フィールド描画
    function renderField() {
      const tbl = document.getElementById('field');
      tbl.innerHTML = '';
      // ヘッダー行
      let header = '<tr><th></th>';
      for (let v = 1; v <= 13; v++) header += `<th>${v}</th>`;
      header += '</tr>';
      tbl.insertAdjacentHTML('beforeend', header);
      // 各スート行
      for (let s of suits) {
        const { min, max } = fieldState[s];
        let row = `<tr><th>${s}</th>`;
        for (let v = 1; v <= 13; v++) {
          row += (v >= min && v <= max)
            ? `<td>${v}</td>`
            : `<td></td>`;
        }
        row += '</tr>';
        tbl.insertAdjacentHTML('beforeend', row);
      }
    }

    // 手札描画(プレイヤーのみ)
    function renderHands() {
      const container = document.getElementById('playerHand');
      container.innerHTML = '';
      // ソート:スート→数字
      players[0].hand.sort((a,b) => {
        const si = suits.indexOf(a.suit), sj = suits.indexOf(b.suit);
        return si !== sj ? si - sj : a.value - b.value;
      });
      players[0].hand.forEach((card, idx) => {
        const div = document.createElement('div');
        div.className = 'card';
        div.textContent = `${card.value}${card.suit}`;
        if (card.suit === '♥' || card.suit === '♦') div.classList.add('red');
        div.onclick = () => playerPlay(idx);
        container.appendChild(div);
      });
    }

    // コンピュータ手札枚数更新
    function updateCompCounts() {
      document.getElementById('comp0Count').textContent = players[1].hand.length;
      document.getElementById('comp1Count').textContent = players[2].hand.length;
      document.getElementById('comp2Count').textContent = players[3].hand.length;
    }

    // 合法手札インデックス取得
    function legalMoves(hand) {
      const moves = [];
      hand.forEach((c, i) => {
        const f = fieldState[c.suit];
        if ((c.value === f.min - 1 && f.min > 1) ||
            (c.value === f.max + 1 && f.max < 13)) {
          moves.push(i);
        }
      });
      return moves;
    }

    // プレイヤー手出し
    function playerPlay(idx) {
      if (gameOver || currentPlayer !== 0) return;
      const moves = legalMoves(players[0].hand);
      if (!moves.includes(idx)) {
        showMessage('そのカードは出せません');
        return;
      }
      playCard(0, idx);
      nextTurn();
    }

    // プレイヤーパス
    function playerPass() {
      if (gameOver || currentPlayer !== 0) return;
      players[0].pass++;
      showMessage(`あなたはパス (${players[0].pass}/${maxPass})`);
      if (players[0].pass > maxPass) {
        eliminate(0);
      }
      nextTurn();
    }

    // カードを場に出す
    function playCard(p, idx) {
      const card = players[p].hand.splice(idx,1)[0];
      const f = fieldState[card.suit];
      if (card.value === f.min - 1) f.min--;
      else f.max++;
      renderField();
      renderHands();
      updateCompCounts();
      showMessage(`${p===0?'あなた':'コンピュータ'+p} が ${card.value}${card.suit} を出しました`);
      checkGameEnd();
    }

    // コンピュータターン
    function computerTurn() {
      if (gameOver) return;
      const p = currentPlayer;  
      const moves = legalMoves(players[p].hand);
      if (moves.length > 0) {
        playCard(p, moves[0]);
      } else {
        players[p].pass++;
        showMessage(`コンピュータ${p} はパス (${players[p].pass}/${maxPass})`);
        if (players[p].pass > maxPass) eliminate(p);
      }
      nextTurn();
    }

    // 次のターン
    function nextTurn() {
      if (gameOver) return;
      for (let i = 1; i <= numPlayers; i++) {
        const np = (currentPlayer + i) % numPlayers;
        if (!players[np].eliminated) {
          currentPlayer = np;
          break;
        }
      }
      updateUI();
      if (!gameOver) {
        if (currentPlayer === 0) {
          showMessage('あなたの番です');
        } else {
          setTimeout(computerTurn, 800);
        }
      }
    }

    // パス上限超過で失格
    function eliminate(p) {
      players[p].eliminated = true;
      showMessage(`${p===0?'あなた':'コンピュータ'+p} は失格しました`);
      checkGameEnd();
    }

    // ゲーム終了判定
    function checkGameEnd() {
      // 手札0枚の勝利
      for (let i = 0; i < numPlayers; i++) {
        if (!players[i].eliminated && players[i].hand.length === 0) {
          gameOver = true;
          showMessage(`${i===0?'あなた':'コンピュータ'+i} の勝利!`);
          endGame();
          return;
        }
      }
      // 生存者1名の勝利
      const alive = players.filter(p => !p.eliminated);
      if (alive.length === 1) {
        gameOver = true;
        const winner = players.findIndex(p => !p.eliminated);
        showMessage(`${winner===0?'あなた':'コンピュータ'+winner} の勝利!`);
        endGame();
      }
    }

    // 終了処理
    function endGame() {
      updateUI();
      document.getElementById('restartBtn').style.display = 'inline-block';
    }

    // UI更新
    function updateUI() {
      document.getElementById('passBtn').disabled = (currentPlayer !== 0 || gameOver);
      document.querySelectorAll('#playerHand .card').forEach((div,idx) => {
        const legal = legalMoves(players[0].hand);
        if (currentPlayer!==0 || gameOver || !legal.includes(idx)) div.classList.add('disabled');
        else div.classList.remove('disabled');
      });
    }

    // メッセージ表示
    function showMessage(msg) {
      document.getElementById('message').textContent = msg;
    }
  </script>
</body>
</html>

アルゴリズムの流れ

ステップ関数/命令内容
初期化initGame()フィールド初期化・デッキ生成・シャッフル・配牌・先攻決定・7自動配置・UI描画・最初のプレイヤー実行
フィールド描画renderField()HTMLテーブルに対し、各スートの minmax のマスに数値を表示
手札描画renderHands()手札をソートし、カード要素を生成。クリックでプレイヤープレイ
合法手判定legalMoves(hand)手札中、自分のスート min-1 または max+1 のカードインデックスを抽出
カード出し/場更新playCard(player,idx)手札からカードを削除→fieldState 更新→UI再描画→メッセージ
パス/失格判定playerPass()eliminate(p)パス回数をカウントし、上限超過で eliminated=true。以降スキップ
ターン管理nextTurn()次の有効プレイヤーを currentPlayer に設定→UI更新→AIなら自動実行
勝敗判定checkGameEnd()手札ゼロまたは生存1名で終了→勝者メッセージ→endGame()
メッセージ表示showMessage(msg)画面下部のメッセージエリアに文字列を表示

関数の詳細

関数名機能
initGame()ゲームをリセットし、場とプレイヤー状態を初期化。最初の7を出し先攻プレイヤーを決定。UIを描画。
createDeck()4スート×1~13 のカードオブジェクト配列を生成
shuffle(d)Fisher–Yates アルゴリズムで配列 d をランダム化
renderField()<table> 要素にフィールド状態を反映。各スート行をヘッダー+数値セル or 空セルで描画
renderHands()プレイヤーの手札をスート・数字順にソートし、.card 要素として表示。クリックで playerPlay() 呼び出し
legalMoves(hand)与えられた手札配列中、自分の場と連続可能なカードインデックスの配列を返却
playCard(p,idx)プレイヤー p の手札からインデックス idx のカードを場に出し、fieldState を更新
playerPass()プレイヤーのパス回数をインクリメント。上限超過なら eliminate() 呼び出し
computerTurn()AIプレイヤーのターン実装:合法手を最初の1枚でプレイ、なければパス
nextTurn()次の有効プレイヤーに交代し、自分/AIの動作を切り替え
checkGameEnd()勝利条件(手札0枚 or 生存者1名)をチェックし、終了なら endGame() 呼び出し
endGame()gameOver=true に設定し、「もう一度」ボタンを表示
updateUI()ボタンと手札の .disabled 状態を制御
showMessage(msg)画面下部のメッセージ要素に msg を表示
nameOf(i)プレイヤー番号を「あなた」 or 「コンピュータn」にマッピング

このゲームの改造ポイント

  • AIの強化:現状は最初の合法手を出すだけ。ミニマックス法やヒューリスティックを導入して相手を邪魔する戦略AIに。
  • 可視化演出:カードの出るアニメーション、パス時のエフェクト、勝利時の花火表示などで臨場感アップ。
  • オンライン対戦:WebSocket で遠隔プレイヤー間のリアルタイム対戦を実装。
  • 手札公開オプション:上級者向けに対戦相手の手札を一部表示するモードを追加。
  • スートと数のカスタマイズ:トランプ以外のテーマ(麻雀牌やドミノなど)に応じてスート・数値を拡張。
  • 観戦モード:第5人目として他プレイヤーの対戦を観戦可能にし、チャット機能やログ再生も実装。

この基本構造をベースに、ぜひオリジナリティあふれる「七並べ」を作り込んでみてください!