UNPKG

koishi-plugin-card-21-game

Version:

Koishi 的 21 点(黑杰克)纸牌游戏插件。完整赌场规则。

905 lines (896 loc) 31.2 kB
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"}) 请发送 &quot;下注 &lt;金额&gt;&quot; 加入游戏。 发送 "开始" 立即发牌。`; }); 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 });