きっかけ
この記事を読んで作りたくなった。
前書き(予防線)
なるべくシンプルに少ないコードで、
バニラ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>