Mô hình hóa luật rút lá thứ ba Baccarat bằng JavaScript thuần
1. Bài toán không đơn giản chỉ là cộng điểm
Khi xây dựng một công cụ kiểm tra luật Baccarat, phần giao diện không phải vấn đề khó nhất. Phần dễ sai nhất là biến bảng luật lá thứ ba thành mã nguồn rõ ràng, có thể kiểm thử và không tạo ra kết luận mơ hồ.
Một vòng Baccarat thường bắt đầu bằng hai lá cho Player và hai lá cho Banker. Giá trị của bài được tính theo hàng đơn vị:
- A có giá trị 1.
- Các lá từ 2 đến 9 giữ nguyên giá trị.
- 10, J, Q và K có giá trị 0.
- Nếu tổng lớn hơn 9, chỉ giữ chữ số hàng đơn vị.
Ví dụ:
- 7 + 8 = 15, tổng Baccarat là 5.
- K + 6 = 6.
- A + 8 = 9.
- 9 + 9 = 18, tổng Baccarat là 8.
Mục tiêu của công cụ trong bài viết này không phải dự đoán kết quả. Nó chỉ nhận dữ liệu đầu vào, áp dụng đúng bảng quyết định và trả về hành động draw, stand, natural hoặc pending.
2. Chuẩn hóa giá trị lá bài
Không nên đặt hàng loạt câu lệnh if trực tiếp trong giao diện. Trước tiên, chúng ta chuẩn hóa lá bài thành giá trị số.
const CARD_VALUES = Object.freeze({
A: 1,
"2": 2,
"3": 3,
"4": 4,
"5": 5,
"6": 6,
"7": 7,
"8": 8,
"9": 9,
"10": 0,
J: 0,
Q: 0,
K: 0,
});
function getCardValue(card) {
const normalized = String(card).trim().toUpperCase();
if (!(normalized in CARD_VALUES)) {
throw new TypeError(`Lá bài không hợp lệ: ${card}`);
}
return CARD_VALUES[normalized];
}
Sử dụng một bảng ánh xạ giúp mã nguồn dễ đọc hơn và tránh việc lặp lại logic chuyển đổi ở nhiều nơi.
Tiếp theo là hàm tính tổng:
function getHandTotal(cards) {
if (!Array.isArray(cards) || cards.length === 0) {
throw new TypeError("Hand phải là một mảng có ít nhất một lá");
}
const total = cards.reduce(
(sum, card) => sum + getCardValue(card),
0
);
return total % 10;
}
Một số ví dụ:
getHandTotal(["7", "8"]); // 5
getHandTotal(["K", "6"]); // 6
getHandTotal(["A", "8"]); // 9
getHandTotal(["9", "9"]); // 8
3. Tách luật Player thành một hàm riêng
Player có bảng quyết định khá ngắn:
- Tổng 8 hoặc 9 là Natural.
- Tổng từ 0 đến 5: rút.
- Tổng 6 hoặc 7: đứng.
function getPlayerAction(total) {
if (total < 0 || total > 9) {
throw new RangeError("Tổng Player phải nằm trong khoảng 0–9");
}
if (total === 8 || total === 9) {
return "natural";
}
return total <= 5 ? "draw" : "stand";
}
Không nên để hàm trả về một câu tiếng Việt hoàn chỉnh. Giá trị draw, stand và natural dễ tái sử dụng hơn cho giao diện web, API hoặc unit test.
Việc chuyển kết quả sang ngôn ngữ hiển thị nên nằm ở một lớp khác:
const ACTION_LABELS = {
draw: "Rút",
stand: "Đứng",
natural: "Natural",
pending: "Chờ lá thứ ba của Player",
};
4. Mô hình hóa luật Banker
Quyết định của Banker phụ thuộc vào hai tình huống khác nhau.
Khi Player đứng
Nếu Player đứng với 6 hoặc 7 điểm:
- Banker rút khi có tổng từ 0 đến 5.
- Banker đứng khi có tổng 6 hoặc 7.
Khi Player rút lá thứ ba
Lúc này Banker phải xét cả tổng của mình và giá trị lá thứ ba của Player:
| Tổng Banker | Điều kiện rút |
|---|---|
| 0–2 | Luôn rút |
| 3 | Rút, trừ khi lá thứ ba Player là 8 |
| 4 | Rút khi lá thứ ba Player từ 2 đến 7 |
| 5 | Rút khi lá thứ ba Player từ 4 đến 7 |
| 6 | Rút khi lá thứ ba Player là 6 hoặc 7 |
| 7 | Luôn đứng |
Có thể chuyển bảng trên thành một hàm thuần:
function shouldBankerDraw(bankerTotal, playerThirdValue = null) {
if (bankerTotal < 0 || bankerTotal > 7) {
throw new RangeError(
"Hàm này chỉ xử lý Banker từ 0 đến 7 sau khi loại Natural"
);
}
// Player đứng, không có lá thứ ba.
if (playerThirdValue === null) {
return bankerTotal <= 5;
}
if (playerThirdValue < 0 || playerThirdValue > 9) {
throw new RangeError(
"Giá trị lá thứ ba của Player phải nằm trong khoảng 0–9"
);
}
if (bankerTotal <= 2) {
return true;
}
if (bankerTotal === 3) {
return playerThirdValue !== 8;
}
if (bankerTotal === 4) {
return playerThirdValue >= 2 && playerThirdValue <= 7;
}
if (bankerTotal === 5) {
return playerThirdValue >= 4 && playerThirdValue <= 7;
}
if (bankerTotal === 6) {
return playerThirdValue === 6 || playerThirdValue === 7;
}
return false;
}
Ưu điểm của cách viết này là mỗi nhánh tương ứng trực tiếp với một hàng trong bảng luật. Khi đọc lại mã nguồn, lập trình viên không cần giải mã một biểu thức Boolean quá dài.
5. Hàm điều phối toàn bộ quyết định
Sau khi có các hàm nhỏ, chúng ta xây dựng hàm điều phối.
Hàm này nhận bốn lá đầu và một lá thứ ba tùy chọn của Player. Nếu chưa có đủ dữ liệu để xác định Banker, nó trả về trạng thái pending.
function decideBaccaratRound({
playerCards,
bankerCards,
playerThirdCard = null,
}) {
if (playerCards.length !== 2 || bankerCards.length !== 2) {
throw new Error(
"Cần đúng hai lá đầu cho Player và hai lá đầu cho Banker"
);
}
const playerTotal = getHandTotal(playerCards);
const bankerTotal = getHandTotal(bankerCards);
// Chỉ cần một bên có 8 hoặc 9 thì vòng đầu là Natural.
if (
playerTotal === 8 ||
playerTotal === 9 ||
bankerTotal === 8 ||
bankerTotal === 9
) {
return {
playerTotal,
bankerTotal,
playerAction: "natural",
bankerAction: "natural",
reason: "Có Natural 8 hoặc 9, cả hai bên đều đứng.",
};
}
const playerAction = getPlayerAction(playerTotal);
if (playerAction === "stand") {
const bankerDraw = shouldBankerDraw(bankerTotal, null);
return {
playerTotal,
bankerTotal,
playerAction: "stand",
bankerAction: bankerDraw ? "draw" : "stand",
reason:
"Player đứng; Banker rút từ 0–5 và đứng ở 6–7.",
};
}
if (playerThirdCard === null) {
return {
playerTotal,
bankerTotal,
playerAction: "draw",
bankerAction: "pending",
reason:
"Cần giá trị lá thứ ba của Player trước khi quyết định Banker.",
};
}
const playerThirdValue = getCardValue(playerThirdCard);
const bankerDraw = shouldBankerDraw(
bankerTotal,
playerThirdValue
);
return {
playerTotal,
bankerTotal,
playerThirdValue,
playerAction: "draw",
bankerAction: bankerDraw ? "draw" : "stand",
reason:
"Quyết định Banker được lấy từ tổng Banker và lá thứ ba của Player.",
};
}
Cấu trúc trả về là một object thay vì chuỗi. Giao diện có thể tự quyết định cách trình bày, trong khi unit test chỉ cần kiểm tra các trường dữ liệu.
6. Kiểm thử bằng Node.js
Với Node.js hiện đại, có thể dùng node:test mà không cần cài thêm thư viện.
import test from "node:test";
import assert from "node:assert/strict";
test("Natural 8 làm cả hai bên đứng", () => {
const result = decideBaccaratRound({
playerCards: ["5", "3"],
bankerCards: ["4", "2"],
});
assert.equal(result.playerTotal, 8);
assert.equal(result.bankerTotal, 6);
assert.equal(result.playerAction, "natural");
assert.equal(result.bankerAction, "natural");
});
test("Player 6 đứng, Banker 5 rút", () => {
const result = decideBaccaratRound({
playerCards: ["4", "2"],
bankerCards: ["3", "2"],
});
assert.equal(result.playerAction, "stand");
assert.equal(result.bankerAction, "draw");
});
test("Banker 3 đứng khi lá thứ ba Player là 8", () => {
const result = decideBaccaratRound({
playerCards: ["2", "3"],
bankerCards: ["A", "2"],
playerThirdCard: "8",
});
assert.equal(result.playerAction, "draw");
assert.equal(result.bankerAction, "stand");
});
test("Banker 4 rút khi lá thứ ba Player là 7", () => {
const result = decideBaccaratRound({
playerCards: ["2", "3"],
bankerCards: ["2", "2"],
playerThirdCard: "7",
});
assert.equal(result.bankerAction, "draw");
});
test("Thiếu lá thứ ba phải trả về pending", () => {
const result = decideBaccaratRound({
playerCards: ["2", "3"],
bankerCards: ["2", "2"],
});
assert.equal(result.playerAction, "draw");
assert.equal(result.bankerAction, "pending");
});
Chạy kiểm thử:
node --test baccarat.test.js
Những test quan trọng nhất nên nằm ở ranh giới của bảng quyết định:
- Banker 3 với lá thứ ba Player là 7 và 8.
- Banker 4 với lá thứ ba Player là 1, 2, 7 và 8.
- Banker 5 với lá thứ ba Player là 3, 4, 7 và 8.
- Banker 6 với lá thứ ba Player là 5, 6, 7 và 8.
- Natural của Player.
- Natural của Banker.
- Player đứng nhưng Banker vẫn phải rút.
7. Kết nối với giao diện web
Giao diện chỉ cần bốn ô chọn cho các lá đầu, một ô tùy chọn cho lá thứ ba của Player và một vùng kết quả có aria-live.
<div id="result" aria-live="polite"></div>
Khi hiển thị kết quả do người dùng nhập, nên dùng textContent thay cho innerHTML:
const output = document.querySelector("#result");
output.textContent =
`Player ${result.playerTotal} điểm: ` +
`${ACTION_LABELS[result.playerAction]}. ` +
`Banker ${result.bankerTotal} điểm: ` +
`${ACTION_LABELS[result.bankerAction]}.`;
Dù dữ liệu chỉ đến từ các thẻ select, giữ nguyên nguyên tắc không ghép dữ liệu đầu vào trực tiếp vào HTML vẫn giúp giảm rủi ro khi công cụ được mở rộng sau này.
Công cụ cũng không cần gửi dữ liệu về máy chủ. Toàn bộ phép tính có thể chạy trong trình duyệt, giúp giao diện phản hồi ngay và giảm phần backend không cần thiết.
8. Kiểm tra công cụ ngoài môi trường test
Unit test xác nhận thuật toán, nhưng vẫn cần kiểm tra thêm:
- Tổng điểm hiển thị có đúng sau phép
% 10không. - Giá trị 10, J, Q và K có cùng bằng 0 không.
- Khi có Natural, lựa chọn lá thứ ba có bị bỏ qua không.
- Khi Player đứng, giao diện có giải thích đúng nhánh Banker không.
- Trạng thái
pendingcó hướng dẫn người dùng chọn lá thứ ba không. - Nội dung có hoạt động trên màn hình điện thoại và bàn phím không.
Một phiên bản minh họa đang áp dụng cùng cách tách hàm, bảng quyết định và trạng thái chờ có thể xem tại công cụ kiểm tra luật rút Baccarat. Liên kết này chỉ dùng để đối chiếu cách triển khai giao diện; công cụ không dự đoán kết quả và không đưa ra cam kết thắng.
Kết luận
Với một bảng luật nhiều điều kiện, cách triển khai ổn định nhất là:
- Chuẩn hóa dữ liệu đầu vào.
- Tách phép tính tổng khỏi quyết định hành động.
- Viết một hàm riêng cho Player.
- Chuyển bảng Banker thành các nhánh có thể đọc trực tiếp.
- Trả về object có cấu trúc.
- Kiểm thử các giá trị biên.
- Để lớp giao diện chỉ chịu trách nhiệm hiển thị.
Cách tổ chức này dài hơn một biểu thức if duy nhất, nhưng dễ kiểm tra, dễ sửa và hạn chế lỗi khi bổ sung giao diện hoặc API sau này.
All rights reserved