1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【JavaScript】超シンプルなブラックジャック

Last updated at Posted at 2023-12-15

きっかけ

この記事を読んで作りたくなった。

前書き(予防線)

なるべくシンプルに少ないコードで、
バニラ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>
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?