きっかけ
この記事を読んで作りたくなった。
前書き(予防線)
なるべくシンプルに少ないコードで、
バニラJSで、
拡張性を別段考慮せず、
ボタン連打によるバグなども一切考慮せず、
演出やUIは全くこだわらず、
1ファイルで完結するようにし、
ロジックが初学者でも(頑張れば)分かるように意識して作ってみました。
詳しいルールはWikipediaを読んでください。(丸投げ)
以下が分かっていればいいです。
- 「2~9」はそのまま
- 「10~K」は10
- 「A」は1 or 11
- 合計が21を超えずに近い方が勝ち
- 両者とも21を超えたらプレイヤーの負け
- ディーラーがカード2枚で合計21、
 プレイヤーがカード3枚以上で合計21の場合、プレイヤーの負け
- HIT = カード追加
- STAND = このままでいい
なおゲームで出てくるカードの「T」は「10」を意味しています。
成果物
コード
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ブラックジャックもどき</title>
    <style>
    body, button {
        font-size: 32px;
        user-select: none;
    }
    .name {
        display: inline-block;
        width: 150px;
        text-align: center;
    }
    #dealer-card-list, #player-card-list {
        display: inline-block;
    }
    #result, #hit, #stand {
        display: inline-block;
        margin-right: 8px;
    }
    #next {
        display: block;
    }
    @media screen and (width <= 480px) {
        body, button {
            font-size: 14px;
        }
        .name {
            width: 70px;
        }
    }
    </style>
</head>
<body>
    <div id="history">WIN:0 / LOSE:0 / DRAW:0</div>
    <div>
        <div class="name">DEALER</div>
        <div id="dealer-card-list"></div>
    </div>
    <div>
        <div class="name">PLAYER</div>
        <div id="player-card-list"></div>
    </div>
    <button id="hit">HIT</button><button id="stand">STAND</button>
    <div id="result"></div><button id="next">NEXT</button>
    <script>
    const domHistory = document.querySelector("#history");
    const domDealerCardList = document.querySelector("#dealer-card-list");
    const domPlayerCardList = document.querySelector("#player-card-list");
    const domResult = document.querySelector("#result");
    const domHit = document.querySelector("#hit");
    const domStand = document.querySelector("#stand");
    const domNext = document.querySelector("#next");
    let winCount = 0, loseCount = 0, drawCount = 0;
    let cardList = [];
    let dealerCardList = [];
    let playerCardList = [];
    /// UI更新処理定義、シーン処理定義
    function updateUICardList(isGameOver = false) {
        if (isGameOver) {
            domDealerCardList.textContent = dealerCardList.map(card => card.suit + card.numberName).join(" ");
        }
        else {
            domDealerCardList.textContent = dealerCardList[0].suit + dealerCardList[0].numberName;
            for (let i = 0; i < dealerCardList.length - 1; i++) {
                domDealerCardList.textContent += " ?";
            }
        }
        domPlayerCardList.textContent = playerCardList.map(card => card.suit + card.numberName).join(" ");
    }
    function gameStartScene() {
        cardList = [];
        for (const suit of "♠♣♢♡") {
            for (const numberName of "A23456789TJQK") {
                cardList.push({suit, numberName});
            }
        }
        dealerCardList = [hit(), hit()];
        playerCardList = [hit(), hit()];
        updateUICardList();
        domResult.style.display = "none";
        domHit.style.display = "";
        domStand.style.display = "";
        domNext.style.display = "none";
    }
    function gameOverScene() {
        const playerSum = calcSum(playerCardList);
        const dealerSum = calcSum(dealerCardList);
        let result = "";
        // バストは強制敗北
        if (playerSum > 21) {
            loseCount++;
            result = "LOSE";
        }
        else if (dealerSum > 21) {
            winCount++;
            result = "WIN";
        }
        else if (playerSum === 21 && dealerSum === 21) {
            if (dealerCardList.length === 2 && playerCardList.length > 2) {
                loseCount++;
                result = "LOSE";
            }
            else {
                drawCount++;
                result = "DRAW";
            }
        }
        else if (playerSum > dealerSum) {
            winCount++;
            result = "WIN";
        }
        else if (playerSum < dealerSum) {
            loseCount++;
            result = "LOSE";
        }
        else {
            drawCount++;
            result = "DRAW";
        }
        updateUICardList(true);
        domResult.style.display = "";
        const displayPlayerSum = playerSum === 21 && playerCardList.length === 2 ? "NATURAL21" : playerSum;
        const displayDealerSum = dealerSum === 21 && dealerCardList.length === 2 ? "NATURAL21" : dealerSum;
        domResult.textContent = `PLAYER:${displayPlayerSum}, DEALER:${displayDealerSum} / ${result}`;
        domHistory.textContent = `WIN:${winCount} / LOSE:${loseCount} / DRAW:${drawCount}`;
        domHit.style.display = "none";
        domStand.style.display = "none";
        domNext.style.display = "";
    }
    /// ビジネスロジック定義
    function calcSum(cardList) {
        let maxAceCount = 0;
        let sum = 0;
        for (const card of cardList) {
            if (card.numberName === "A") {
                maxAceCount++;
            }
            else if ("TJQK".includes(card.numberName)) {
                sum += 10;
            }
            else {
                sum += Number(card.numberName);
            }
        }
        // 「♠A ♣A ♢A ♡A ♠8 ♠9」みたいなケースがあるのでエースは最後に足す
        const tmpSum = sum;
        for (let aceCount = 0; aceCount <= maxAceCount; aceCount++) {
            sum = tmpSum + 11 * (maxAceCount - aceCount) + 1 * aceCount;
            if (sum <= 21) {
                return sum;
            }
        }
        return sum;
    }
    function hit() {
        const index = Math.floor(Math.random() * cardList.length);
        const card = cardList[index];
        cardList.splice(index, 1);
        return card;
    }
    function dealerTurn(isPlayerStand = false) {
        if (calcSum(dealerCardList) >= 17) {
            return;
        }
        dealerCardList.push(hit());
        if (isPlayerStand) {
            dealerTurn(true);
        }
    }
    /// イベント定義
    domHit.onclick = () => {
        playerCardList.push(hit());
        if (calcSum(playerCardList) > 21) {
            gameOverScene();
            return;
        }
        dealerTurn();
        updateUICardList();
    };
    domStand.onclick = () => {
        dealerTurn(true);
        gameOverScene();
    };
    domNext.onclick = () => {
        gameStartScene();
    };
    /// ゲーム開始
    gameStartScene();
    </script>
</body>
</html>