koishi-plugin-card-21-game
Version:
Koishi 的 21 点(黑杰克)纸牌游戏插件。完整赌场规则。
905 lines (896 loc) • 31.2 kB
JavaScript
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
var __export = (target, all) => {
for (var name2 in all)
__defProp(target, name2, { get: all[name2], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var src_exports = {};
__export(src_exports, {
Config: () => Config,
apply: () => apply,
inject: () => inject,
name: () => name,
usage: () => usage
});
module.exports = __toCommonJS(src_exports);
var import_koishi = require("koishi");
var name = "card-21-game";
var inject = ["database", "monetary"];
var usage = `
## 🃏 21点 (Blackjack)
还原真实赌场规则,支持分牌、双倍、保险及投降。
### 🎮 快速开始
1. **发起游戏**: 输入 \`blackjack.来一局\`。
- **PVE模式 (默认)**: 玩家对抗庄家(Bot)。
- **PVP模式 (无庄)**: 输入 \`blackjack.来一局 -n\`,玩家之间互相比大小。
2. **加入**: 游戏创建后,输入 \`下注 100\` 或 \`bet 100\` 加入。
3. **开始**: 所有玩家加入后,输入 \`开始\` 发牌 (倒计时结束也会自动开始)。
### 🕹️ 游戏操作
轮到你时,直接发送指令:
- **要牌 (Hit)**: \`要牌\`, \`hit\`, \`h\`
- **停牌 (Stand)**: \`停牌\`, \`stand\`, \`s\`
- **加倍 (Double)**: \`加倍\`, \`double\`, \`d\` (仅首轮,注金翻倍,只发一张)
- **分牌 (Split)**: \`分牌\`, \`split\`, \`p\` (仅起手对子,注金翻倍)
- **投降 (Surrender)**: \`投降\` (仅开局前5秒,输一半)
- **保险 (Insurance)**: \`保险\` (仅庄家明牌为A,保一半)
### ⚙️ 规则说明
- **Blackjack (3:2)**: 起手2张牌直接21点。分牌后的21点不算BJ。
- **庄家规则**: 庄家点数 < 17 必须要牌,>= 17 停牌。
- **分A特例**: 分A后每家只发一张牌,强制结束该手牌。
`;
var Config = import_koishi.Schema.object({
minBet: import_koishi.Schema.number().default(10).description("最低起注金额。"),
deckCount: import_koishi.Schema.number().default(4).min(1).max(8).description("使用牌堆数量 (一副52张)。"),
playerTurnTimeout: import_koishi.Schema.number().default(30).description("玩家操作超时时间(秒)。"),
joinPhaseTimeout: import_koishi.Schema.number().default(45).description("加入阶段等待超时时间(秒)。"),
currency: import_koishi.Schema.union(["monetary", "bella"]).default("monetary").description("使用的货币系统 (Monetary通用/Bella插件)。"),
currencyName: import_koishi.Schema.string().default("default").description("货币名称(仅monetary模式有效)。"),
dealerHitSoft17: import_koishi.Schema.boolean().default(false).description("庄家是否在软17(A+6)时要牌 (目前仅做预留,默认>=17停牌)。")
});
var Ok = /* @__PURE__ */ __name((value) => ({ ok: true, value }), "Ok");
var Err = /* @__PURE__ */ __name((error) => ({ ok: false, error }), "Err");
var CARDS_TEMPLATE = [
"♥️A",
"♥️2",
"♥️3",
"♥️4",
"♥️5",
"♥️6",
"♥️7",
"♥️8",
"♥️9",
"♥️10",
"♥️J",
"♥️Q",
"♥️K",
"♦️A",
"♦️2",
"♦️3",
"♦️4",
"♦️5",
"♦️6",
"♦️7",
"♦️8",
"♦️9",
"♦️10",
"♦️J",
"♦️Q",
"♦️K",
"♣️A",
"♣️2",
"♣️3",
"♣️4",
"♣️5",
"♣️6",
"♣️7",
"♣️8",
"♣️9",
"♣️10",
"♣️J",
"♣️Q",
"♣️K",
"♠️A",
"♠️2",
"♠️3",
"♠️4",
"♠️5",
"♠️6",
"♠️7",
"♠️8",
"♠️9",
"♠️10",
"♠️J",
"♠️Q",
"♠️K"
];
function calcScore(hand) {
let sum = 0;
let aces = 0;
for (const card of hand) {
const match = card.match(/[0-9JQKA]+$/);
const valStr = match ? match[0] : "0";
if (["J", "Q", "K", "10"].includes(valStr)) {
sum += 10;
} else if (valStr === "A") {
sum += 11;
aces++;
} else {
sum += parseInt(valStr);
}
}
while (sum > 21 && aces > 0) {
sum -= 10;
aces--;
}
return sum;
}
__name(calcScore, "calcScore");
function isBlackjack(hand) {
if (hand.fromSplit) return false;
return hand.cards.length === 2 && calcScore(hand.cards) === 21;
}
__name(isBlackjack, "isBlackjack");
function getCardRank(card) {
const match = card.match(/[0-9JQKA]+$/);
return match ? match[0] : "";
}
__name(getCardRank, "getCardRank");
function getCardValue(card) {
const r = getCardRank(card);
if (["J", "Q", "K", "10"].includes(r)) return 10;
if (r === "A") return 11;
return parseInt(r);
}
__name(getCardValue, "getCardValue");
var GameSession = class {
static {
__name(this, "GameSession");
}
phase = 0 /* Idle */;
channelId;
players = [];
deck = [];
dealerHand = [];
currentPlayerIndex = 0;
isNoDealerMode = false;
// 防止整个游戏状态下的并发处理
_processing = false;
timer = null;
ctx;
config;
constructor(ctx, config, channelId) {
this.ctx = ctx;
this.config = config;
this.channelId = channelId;
}
// --- Core Lifecycle ---
async init(isNoDealer) {
this.phase = 1 /* Joining */;
this.isNoDealerMode = isNoDealer;
this.players = [];
this.deck = [];
this.setTimer(() => this.handleJoinTimeout(), this.config.joinPhaseTimeout);
}
async join(userId, username, platform, bet) {
if (this.phase !== 1 /* Joining */) return "🚫 游戏已经开始或未初始化。";
if (this._processing) return "";
if (this.players.find((p) => p.userId === userId)) return "⚠️ 你已经加入了。";
if (bet < this.config.minBet) return `⚠️ 最低下注金额为 ${this.config.minBet}。`;
this._processing = true;
const paid = await this.charge(userId, platform, bet);
this._processing = false;
if (!paid) return `💸 余额不足,无法下注 ${bet}。`;
this.players.push({
userId,
username,
platform,
bet,
hands: [{
cards: [],
bet,
isFinished: false,
isDoubled: false,
isSurrendered: false,
insurance: 0,
fromSplit: false
}],
currentHandIndex: 0,
isBusy: false
});
this.setTimer(() => this.handleJoinTimeout(), this.config.joinPhaseTimeout);
return `✅ ${username} 加入成功 (下注 ${bet})。当前玩家: ${this.players.length}人。`;
}
async start() {
if (this.phase !== 1 /* Joining */) return Err("不在准备阶段");
if (this.players.length === 0) return Err("没有玩家");
if (this.isNoDealerMode && this.players.length < 2) return Err("PVP模式至少需要2人");
this.clearTimer();
this.phase = 2 /* Distributing */;
this._processing = true;
this.deck = [];
for (let i = 0; i < this.config.deckCount; i++) this.deck.push(...CARDS_TEMPLATE);
this.shuffle(this.deck);
for (const p of this.players) {
p.hands[0].cards.push(this.drawCard());
}
if (!this.isNoDealerMode) this.dealerHand.push(this.drawCard());
await (0, import_koishi.sleep)(500);
for (const p of this.players) {
p.hands[0].cards.push(this.drawCard());
}
if (!this.isNoDealerMode) this.dealerHand.push(this.drawCard());
await this.renderTable("🃏 游戏开始!发牌完毕。");
this._processing = false;
if (!this.isNoDealerMode && this.dealerHand.length > 0 && this.dealerHand[0].endsWith("A")) {
this.phase = 3 /* Insurance */;
await this.broadcast("💡 庄家明牌为 A,是否购买保险?(回复 '保险' / '跳过')");
this.setTimer(() => this.endInsurancePhase(), 10);
return Ok(void 0);
}
await this.startSurrenderPhase();
return Ok(void 0);
}
// --- Phase Logic ---
async startSurrenderPhase() {
this.phase = 4 /* Surrender */;
await this.broadcast("🏳️ 投降阶段:如牌型不佳,可输入 '投降' (输一半)。\n⏳ 5秒后自动开始玩家回合。");
this.setTimer(() => this.startPlayerTurns(), 5);
}
async endInsurancePhase() {
await this.broadcast("⏰ 保险阶段结束。");
await this.startSurrenderPhase();
}
async startPlayerTurns() {
this.clearTimer();
this.phase = 5 /* PlayerTurn */;
this.currentPlayerIndex = 0;
await this.processCurrentPlayerTurn();
}
// 递归处理玩家回合
async processCurrentPlayerTurn() {
this.clearTimer();
if (this.currentPlayerIndex >= this.players.length) {
return this.startDealerTurn();
}
const player = this.players[this.currentPlayerIndex];
const hand = player.hands[player.currentHandIndex];
if (hand.isFinished || hand.isSurrendered) {
return this.nextHandOrPlayer();
}
if (isBlackjack(hand)) {
await this.broadcast(`⚡️ ${player.username} 拿到 Blackjack!`);
hand.isFinished = true;
return this.nextHandOrPlayer();
}
const score = calcScore(hand.cards);
if (score >= 21) {
hand.isFinished = true;
const reason = score > 21 ? "💥 爆牌" : "🛑 21点";
if (score > 21) await this.broadcast(`💥 ${player.username} ${reason} (${score})`);
return this.nextHandOrPlayer();
}
let prompt = `👉 轮到 ${player.username}`;
if (player.hands.length > 1) {
prompt += ` (手牌 ${player.currentHandIndex + 1}/${player.hands.length})`;
}
prompt += `
🃏 当前牌: ${hand.cards.join("")} [${score}]`;
const canSplit = this.checkCanSplit(player);
const canDouble = hand.cards.length === 2 && !hand.fromSplit;
const actions = ["要牌", "停牌"];
if (canDouble) actions.push("加倍");
if (canSplit) actions.push("分牌");
prompt += `
指令: ${actions.join(" | ")}`;
await this.broadcast(prompt);
this.setTimer(async () => {
await this.broadcast(`⏰ ${player.username} 操作超时,自动停牌。`);
await this.actionStand(player.userId);
}, this.config.playerTurnTimeout);
}
async nextHandOrPlayer() {
const player = this.players[this.currentPlayerIndex];
if (player.currentHandIndex < player.hands.length - 1) {
player.currentHandIndex++;
setTimeout(() => this.processCurrentPlayerTurn(), 800);
return;
}
this.currentPlayerIndex++;
setTimeout(() => this.processCurrentPlayerTurn(), 800);
}
async startDealerTurn() {
this.phase = 6 /* DealerTurn */;
this.clearTimer();
if (this.isNoDealerMode) {
return this.settleGame();
}
await this.broadcast(`👨💼 庄家亮牌: ${this.dealerHand.join("")} [${calcScore(this.dealerHand)}]`);
await (0, import_koishi.sleep)(1e3);
while (calcScore(this.dealerHand) < 17) {
const card = this.drawCard();
this.dealerHand.push(card);
await this.broadcast(`👨💼 庄家要牌: ${card} -> [${calcScore(this.dealerHand)}]`);
await (0, import_koishi.sleep)(1500);
}
const dScore = calcScore(this.dealerHand);
const resultStr = dScore > 21 ? "💥 庄家爆牌!" : `庄家最终点数: ${dScore}`;
await this.broadcast(resultStr);
return this.settleGame();
}
// --- Actions ---
// 所有 Action 返回 string 作为回复内容,如果返回空则不回复
async actionHit(userId) {
if (this._processing) return "";
const ctx = this.getCurrentCtx(userId);
if (!ctx) return "";
const { p, h } = ctx;
this._processing = true;
const card = this.drawCard();
h.cards.push(card);
const score = calcScore(h.cards);
let msg = `🃏 ${p.username} 要牌: ${card} -> [${score}]`;
if (score >= 21) {
h.isFinished = true;
}
this._processing = false;
setTimeout(() => this.processCurrentPlayerTurn(), 500);
return msg;
}
async actionStand(userId) {
if (this._processing) return "";
const ctx = this.getCurrentCtx(userId);
if (!ctx) return "";
const { p, h } = ctx;
this._processing = true;
h.isFinished = true;
const msg = `🛑 ${p.username} 停牌 [${calcScore(h.cards)}]`;
this._processing = false;
setTimeout(() => this.processCurrentPlayerTurn(), 100);
return msg;
}
async actionDouble(userId) {
if (this._processing) return "";
const ctx = this.getCurrentCtx(userId);
if (!ctx) return "";
const { p, h } = ctx;
if (h.cards.length !== 2) return "⚠️ 只能在首轮加倍。";
if (h.fromSplit) return "⚠️ 分牌后不支持加倍。";
this._processing = true;
const paid = await this.charge(userId, p.platform, h.bet);
if (!paid) {
this._processing = false;
return "💸 余额不足,无法加倍。";
}
h.bet *= 2;
h.isDoubled = true;
const card = this.drawCard();
h.cards.push(card);
h.isFinished = true;
const score = calcScore(h.cards);
const msg = `💰 ${p.username} 加倍! 下注 ${h.bet}。发牌: ${card} -> [${score}]`;
this._processing = false;
setTimeout(() => this.processCurrentPlayerTurn(), 1e3);
return msg;
}
async actionSplit(userId) {
if (this._processing) return "";
const ctx = this.getCurrentCtx(userId);
if (!ctx) return "";
const { p, h } = ctx;
if (!this.checkCanSplit(p)) return "⚠️ 无法分牌。";
this._processing = true;
const paid = await this.charge(userId, p.platform, h.bet);
if (!paid) {
this._processing = false;
return "💸 余额不足,无法分牌。";
}
const card1 = h.cards[0];
const card2 = h.cards[1];
const isSplitAces = getCardRank(card1) === "A";
h.cards = [card1, this.drawCard()];
h.fromSplit = true;
if (isSplitAces) h.isFinished = true;
p.hands.push({
cards: [card2, this.drawCard()],
bet: h.bet,
isFinished: isSplitAces,
// 分A直接结束
isDoubled: false,
isSurrendered: false,
insurance: 0,
fromSplit: true
});
let msg = `🔱 ${p.username} 完成分牌!`;
if (isSplitAces) msg += " (分A只发一张牌)";
this._processing = false;
setTimeout(() => this.processCurrentPlayerTurn(), 1e3);
return msg;
}
async actionSurrender(userId) {
if (this.phase !== 4 /* Surrender */) return "";
const p = this.players.find((x) => x.userId === userId);
if (!p || p.hands[0].isSurrendered) return "";
p.hands[0].isSurrendered = true;
p.hands[0].isFinished = true;
return `🏳️ ${p.username} 选择投降 (保留一半注金)。`;
}
async actionInsurance(userId) {
if (this.phase !== 3 /* Insurance */) return "";
const p = this.players.find((x) => x.userId === userId);
if (!p || p.hands[0].insurance > 0) return "";
const cost = p.hands[0].bet / 2;
const paid = await this.charge(userId, p.platform, cost);
if (!paid) return "💸 余额不足买保险。";
p.hands[0].insurance = cost;
return `🛡️ ${p.username} 购买了保险 (花费 ${cost})。`;
}
// --- Settlement ---
async settleGame() {
this.phase = 7 /* Settlement */;
this._processing = true;
let report = "📊 结算报告\n";
report += "----------------\n";
if (this.isNoDealerMode) {
await this.settlePVP(report);
} else {
await this.settlePVE(report);
}
}
async settlePVE(reportPrefix) {
const dScore = calcScore(this.dealerHand);
const dIsBj = this.dealerHand.length === 2 && dScore === 21;
const dIsBust = dScore > 21;
for (const p of this.players) {
let pTotalProfit = 0;
let pReport = `${p.username}: `;
for (const hand of p.hands) {
if (hand.isSurrendered) {
const refund = hand.bet / 2;
await this.payout(p.userId, p.platform, refund);
pTotalProfit -= refund;
pReport += `[🏳️投降] `;
continue;
}
if (hand.insurance > 0) {
if (dIsBj) {
const insReturn = hand.insurance * 3;
await this.payout(p.userId, p.platform, insReturn);
pTotalProfit += insReturn - hand.insurance;
pReport += `[🛡️保赢] `;
} else {
pTotalProfit -= hand.insurance;
pReport += `[🛡️保亏] `;
}
}
const pScore = calcScore(hand.cards);
const pIsBj = isBlackjack(hand);
let handWinAmount = 0;
let handStatus = "";
if (pScore > 21) {
handStatus = `💥爆(-${hand.bet})`;
pTotalProfit -= hand.bet;
} else if (pIsBj) {
if (dIsBj) {
handWinAmount = hand.bet;
handStatus = `🤝BJ平`;
} else {
handWinAmount = hand.bet * 2.5;
handStatus = `⚡️BJ胜(+${hand.bet * 1.5})`;
pTotalProfit += hand.bet * 1.5;
}
} else if (dIsBj) {
handStatus = `❌败(-${hand.bet})`;
pTotalProfit -= hand.bet;
} else if (dIsBust) {
handWinAmount = hand.bet * 2;
handStatus = `🎉胜(+${hand.bet})`;
pTotalProfit += hand.bet;
} else if (pScore > dScore) {
handWinAmount = hand.bet * 2;
handStatus = `🎉胜(+${hand.bet})`;
pTotalProfit += hand.bet;
} else if (pScore === dScore) {
handWinAmount = hand.bet;
handStatus = `🤝平`;
} else {
handStatus = `❌败(-${hand.bet})`;
pTotalProfit -= hand.bet;
}
if (handWinAmount > 0) {
await this.payout(p.userId, p.platform, handWinAmount);
}
pReport += `${handStatus} `;
}
await this.recordStat(p.userId, p.username, pTotalProfit);
reportPrefix += `${pReport}
`;
}
await this.broadcast(reportPrefix);
this.destroy();
}
async settlePVP(reportPrefix) {
const activePlayers = this.players.filter((p) => !p.hands[0].isSurrendered);
const validPlayers = activePlayers.filter((p) => calcScore(p.hands[0].cards) <= 21);
let pool = 0;
for (const p of this.players) {
if (p.hands[0].isSurrendered) {
await this.payout(p.userId, p.platform, p.bet / 2);
pool += p.bet / 2;
reportPrefix += `${p.username}: 🏳️ 投降
`;
await this.recordStat(p.userId, p.username, -p.bet / 2);
} else {
pool += p.bet;
}
}
if (validPlayers.length === 0) {
reportPrefix += "🤷 全员爆牌/投降,系统收回剩余注金。";
} else {
validPlayers.sort((a, b) => {
const hA = a.hands[0];
const hB = b.hands[0];
const bjA = isBlackjack(hA);
const bjB = isBlackjack(hB);
const sA = calcScore(hA.cards);
const sB = calcScore(hB.cards);
if (bjA && !bjB) return -1;
if (!bjA && bjB) return 1;
return sB - sA;
});
const winners = [validPlayers[0]];
const bestHand = validPlayers[0].hands[0];
for (let i = 1; i < validPlayers.length; i++) {
const p = validPlayers[i];
const h = p.hands[0];
const sameBJ = isBlackjack(bestHand) === isBlackjack(h);
const sameScore = calcScore(bestHand.cards) === calcScore(h.cards);
if (sameBJ && sameScore) {
winners.push(p);
} else {
break;
}
}
const winnerIds = new Set(winners.map((w) => w.userId));
for (const p of this.players) {
if (!winnerIds.has(p.userId) && !p.hands[0].isSurrendered) {
reportPrefix += `${p.username}: ❌ 输 (-${p.bet})
`;
await this.recordStat(p.userId, p.username, -p.bet);
}
}
const totalWin = pool;
const perWin = Math.floor(totalWin / winners.length);
for (const w of winners) {
await this.payout(w.userId, w.platform, perWin);
const profit = perWin - w.bet;
reportPrefix += `${w.username}: 🏆 赢 (+${profit})
`;
await this.recordStat(w.userId, w.username, profit);
}
}
await this.broadcast(reportPrefix);
this.destroy();
}
// --- Utilities ---
getCurrentCtx(userId) {
if (this.phase !== 5 /* PlayerTurn */) return null;
const p = this.players[this.currentPlayerIndex];
if (!p || p.userId !== userId) return null;
return { p, h: p.hands[p.currentHandIndex] };
}
checkCanSplit(p) {
if (p.hands.length >= 2) return false;
const h = p.hands[p.currentHandIndex];
if (h.cards.length !== 2) return false;
return getCardValue(h.cards[0]) === getCardValue(h.cards[1]);
}
drawCard() {
if (this.deck.length === 0) {
for (let i = 0; i < this.config.deckCount; i++) this.deck.push(...CARDS_TEMPLATE);
this.shuffle(this.deck);
}
return this.deck.shift();
}
shuffle(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
}
// --- Economy & IO ---
async charge(userId, platform, amount) {
try {
if (this.config.currency === "bella") {
const rows = await this.ctx.database.get("bella_sign_in", { id: userId });
if (!rows[0] || rows[0].point < amount) return false;
await this.ctx.database.set("bella_sign_in", { id: userId }, { point: rows[0].point - amount });
return true;
} else {
const user = await this.ctx.database.getUser(platform, userId);
if (!user) return false;
const wallet = await this.ctx.monetary?.cost(user.id, amount, this.config.currencyName);
return true;
}
} catch (e) {
return false;
}
}
async payout(userId, platform, amount) {
if (amount <= 0) return;
try {
if (this.config.currency === "bella") {
const rows = await this.ctx.database.get("bella_sign_in", { id: userId });
if (rows[0]) {
await this.ctx.database.set("bella_sign_in", { id: userId }, { point: rows[0].point + amount });
}
} else {
const user = await this.ctx.database.getUser(platform, userId);
if (user) {
await this.ctx.monetary?.gain(user.id, amount, this.config.currencyName);
}
}
} catch (e) {
}
}
async recordStat(userId, username, profit) {
try {
const stats = await this.ctx.database.get("blackjack_stats", { userId });
let stat = stats[0];
if (!stat) {
stat = await this.ctx.database.create("blackjack_stats", {
userId,
username,
wins: 0,
loses: 0,
draws: 0,
bjCount: 0,
totalProfit: 0
});
}
const update = {
totalProfit: stat.totalProfit + profit
};
if (profit > 0) update.wins = stat.wins + 1;
else if (profit < 0) update.loses = stat.loses + 1;
else update.draws = stat.draws + 1;
await this.ctx.database.set("blackjack_stats", { id: stat.id }, update);
} catch (e) {
}
}
async broadcast(msg) {
if (!msg) return;
try {
await this.ctx.bots[0]?.sendMessage(this.channelId, msg);
} catch {
}
}
async renderTable(footer = "") {
let msg = `♠️♣️ Blackjack Table ♥️♦️
`;
if (!this.isNoDealerMode) {
const showHole = this.phase === 7 /* Settlement */ || this.phase === 6 /* DealerTurn */;
let dealerCards;
let dealerScoreStr = "";
if (this.dealerHand.length === 0) {
dealerCards = [];
} else if (showHole) {
dealerCards = this.dealerHand;
dealerScoreStr = ` [${calcScore(dealerCards)}]`;
} else {
dealerCards = [this.dealerHand[0], "🎴"];
dealerScoreStr = ` [?]`;
}
msg += `👨💼 庄家: ${dealerCards.join("")}${dealerScoreStr}
`;
}
for (const p of this.players) {
msg += `👤 ${p.username} ($${p.bet}): ${p.hands.map((h) => {
const status = [];
if (h.isSurrendered) status.push("🏳️");
if (h.isDoubled) status.push("💰");
if (h.insurance) status.push("🛡️");
if (h.fromSplit) status.push("🔱");
return `${h.cards.join("")} [${calcScore(h.cards)}] ${status.join("")}`;
}).join(" | ")}
`;
}
msg += `
${footer}`;
await this.broadcast(msg);
}
setTimer(fn, sec) {
this.clearTimer();
this.timer = setTimeout(fn, sec * 1e3);
}
clearTimer() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
}
async handleJoinTimeout() {
if (this.players.length === 0) {
await this.broadcast("🕐 无人加入,游戏取消。");
this.destroy();
return;
}
if (this.isNoDealerMode && this.players.length < 2) {
await this.broadcast("🕐 人数不足,PVP模式取消,退还注金。");
await this.refundAll();
this.destroy();
return;
}
await this.broadcast("🕐 准备时间结束,自动开始!");
await this.start();
}
async refundAll() {
for (const p of this.players) {
await this.payout(p.userId, p.platform, p.bet);
}
}
destroy() {
this.clearTimer();
this.phase = 8 /* Ended */;
games.delete(this.channelId);
}
};
var games = /* @__PURE__ */ new Map();
function apply(ctx, config) {
ctx.model.extend("blackjack_stats", {
id: "unsigned",
userId: "string",
username: "string",
wins: "unsigned",
loses: "unsigned",
draws: "unsigned",
bjCount: "unsigned",
totalProfit: "double"
}, { primary: "id", autoInc: true });
ctx.middleware(async (session, next) => {
const game = games.get(session.channelId);
if (!game || game.phase === 0 /* Idle */ || game.phase === 8 /* Ended */) return next();
const msg = session.content.trim().toLowerCase();
const uid = session.userId;
const uname = session.username || uid;
if (game.phase === 1 /* Joining */) {
if (msg.startsWith("下注") || msg.startsWith("bet")) {
const amount = parseInt(msg.match(/\d+/)?.[0] || "0");
if (amount > 0) {
await session.send(await game.join(uid, uname, session.platform, amount));
return;
}
}
if (["开始", "start"].includes(msg)) {
const res = await game.start();
if (!res.ok) await session.send(`🚫 ${res.error}`);
return;
}
}
if (game.phase === 3 /* Insurance */) {
if (["保险", "yes", "insure"].includes(msg)) {
await session.send(await game.actionInsurance(uid));
return;
}
if (["跳过", "no", "skip"].includes(msg)) return;
}
if (game.phase === 4 /* Surrender */) {
if (["投降", "surrender"].includes(msg)) {
await session.send(await game.actionSurrender(uid));
return;
}
if (["开始", "继续", "start"].includes(msg)) {
await game.startPlayerTurns();
return;
}
}
if (game.phase === 5 /* PlayerTurn */) {
const p = game.players[game.currentPlayerIndex];
if (p && p.userId === uid) {
if (["要牌", "hit", "h"].includes(msg)) {
await session.send(await game.actionHit(uid));
return;
}
if (["停牌", "stand", "s"].includes(msg)) {
await session.send(await game.actionStand(uid));
return;
}
if (["加倍", "double", "d"].includes(msg)) {
await session.send(await game.actionDouble(uid));
return;
}
if (["分牌", "split", "p"].includes(msg)) {
await session.send(await game.actionSplit(uid));
return;
}
}
}
return next();
});
ctx.command("blackjack", "21点游戏").action(() => `🃏 21点
指令
▸ blackjack.来一局 [-n] 创建 (加 -n 为PVP)
▸ blackjack.强制结束 结束当前游戏
▸ blackjack.战绩 查询战绩
核心规则
▸ BJ赔3:2 庄<17必拿 分A只发一张`);
ctx.command("blackjack.来一局", "创建新游戏").option("nodealer", "-n PVP模式(无庄家)").action(async ({ session, options }) => {
if (games.has(session.channelId)) {
return "🚫 当前频道已有游戏正在进行。";
}
const game = new GameSession(ctx, config, session.channelId);
games.set(session.channelId, game);
await game.init(!!options.nodealer);
return `🎰 21点游戏已创建 (${options.nodealer ? "PVP" : "PVE"})
请发送 "下注 <金额>" 加入游戏。
发送 "开始" 立即发牌。`;
});
ctx.command("blackjack.强制结束", "强制结束").action(async ({ session }) => {
const game = games.get(session.channelId);
if (game) {
await game.refundAll();
game.destroy();
return "✅ 游戏已强制结束,注金已退回。";
}
return "❓ 当前没有进行中的游戏。";
});
ctx.command("blackjack.战绩", "查询个人战绩").action(async ({ session }) => {
try {
const rows = await ctx.database.get("blackjack_stats", { userId: session.userId });
if (rows.length === 0) return "📭 你还没有玩过。";
const s = rows[0];
const total = s.wins + s.loses + s.draws;
const rate = total > 0 ? (s.wins / total * 100).toFixed(1) : "0.0";
return `📊 ${s.username} 的战绩
💰 总盈亏: ${s.totalProfit > 0 ? "+" : ""}${s.totalProfit}
🏆 胜: ${s.wins} | ❌ 负: ${s.loses} | 🤝 平: ${s.draws}
📈 胜率: ${rate}%`;
} catch (e) {
return "⚠️ 无法获取战绩,数据库可能未初始化。";
}
});
ctx.command("blackjack.排行", "查看盈亏排行榜").alias("blackjack.rank").option("limit", "-l <number> 显示数量", { fallback: 10 }).action(async ({ options }) => {
try {
const data = await ctx.database.select("blackjack_stats").orderBy("totalProfit", "desc").limit(Math.min(options.limit, 20)).execute();
if (data.length === 0) return "📊 暂时没有排名数据。";
const list = data.map((s, index) => {
const symbol = s.totalProfit > 0 ? "+" : "";
const prefix = index === 0 ? "🥇" : index === 1 ? "🥈" : index === 2 ? "🥉" : `${index + 1}.`;
return `${prefix} ${s.username}: ${symbol}${s.totalProfit}`;
}).join("\n");
return `🏆 21点 盈亏排行榜 (Top ${data.length})
----------------
${list}`;
} catch (e) {
return "⚠️ 查询排行榜失败,数据库可能未初始化。";
}
});
ctx.on("dispose", () => {
for (const game of games.values()) {
game.refundAll();
game.destroy();
}
games.clear();
});
}
__name(apply, "apply");
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
Config,
apply,
inject,
name,
usage
});