first(); if (! $user) { return; } // 2. 资金管理:自动存款与领取补偿 $this->manageFinances($user, $aiFinance); $round = $this->round->fresh(); if (! $round || ! $round->isBettingOpen()) { return; } // 3. 检查连输惩罚超时 if (Redis::exists('ai_baccarat_timeout')) { return; // 还在禁赛期 } // 4. 检查余额与限额 $config = GameConfig::forGame('baccarat')?->params ?? []; $minBet = (int) ($config['min_bet'] ?? 100); $maxBet = (int) ($config['max_bet'] ?? 50000); // 5. 查询当前是否命中“你玩游戏我买单”活动窗口。 $lossCoverService = app(BaccaratLossCoverService::class); $lossCoverEvent = $lossCoverService->findEventForBetTime(now()); $isInLossCoverWindow = $lossCoverEvent !== null; // 买单活动进行中时允许 AI 统筹“手上 + 银行”总资产;平时只动用当前手上的 jjb。 $bettableGold = $isInLossCoverWindow ? $aiFinance->getTotalGoldAssets($user) : $aiFinance->getSpendableGold($user); if ($bettableGold < $minBet) { return; // 资金不足以支撑最小下注 } // 6. 获取近期路单和 AI 历史下注 $recentResults = BaccaratRound::query() ->where('status', 'settled') ->orderByDesc('id') ->limit(20) ->pluck('result') ->toArray(); $historicalBets = BaccaratBet::query() ->join('baccarat_rounds', 'baccarat_bets.round_id', '=', 'baccarat_rounds.id') ->where('baccarat_bets.user_id', $user->id) ->where('baccarat_rounds.status', 'settled') ->orderByDesc('baccarat_rounds.id') ->limit(10) ->select('baccarat_bets.*') ->get() ->map(function ($bet) { return [ 'round_id' => $bet->round_id, 'bet_type' => $bet->bet_type, 'amount' => $bet->amount, 'profit' => $bet->profit ?? 0, ]; }) ->toArray(); // 7. 调用 AI 接口出具统筹策略 $predictionService = app(BaccaratPredictionService::class); $context = [ 'recent_results' => $recentResults, 'available_gold' => $bettableGold, 'historical_bets' => $historicalBets, 'loss_cover_active' => $isInLossCoverWindow, ]; $aiPrediction = $predictionService->predict($context); $decisionSource = 'ai'; $betType = 'pass'; $amount = 0; $reason = ''; if ($aiPrediction) { $betType = $aiPrediction['action']; $percent = $aiPrediction['percentage']; $reason = $aiPrediction['reason']; // 买单活动期间不允许 AI 选择观望,避免错过“输也可返还”的活动资格。 if ($isInLossCoverWindow && $betType === 'pass') { $decisionSource = 'local'; $betType = $this->resolveLocalBetType($recentResults, false); $reason = trim($reason.' 买单活动进行中,本局禁止观望,已切换本地强制参战策略。'); } if ($betType !== 'pass') { if ($isInLossCoverWindow) { // 买单活动进行中且金币足够时,直接按百家乐单局最高限额下注。 $amount = min($bettableGold, $maxBet); $reason = trim($reason.' 买单活动进行中,本局按百家乐最高限额下注。'); } else { // 非买单活动期间,限定单局最高下注不超过手头金币的 5% 以防止 AI 破产。 $percent = min(5, max(0, $percent)); $amount = (int) round($bettableGold * ($percent / 100.0)); $amount = max($minBet, min($amount, $maxBet)); if ($amount > $bettableGold) { $amount = $bettableGold; } } } } else { // AI 不可用时回退本地路单决策(保底逻辑) $decisionSource = 'local'; // 买单活动期间,本地兜底策略同样不能返回观望。 $betType = $this->resolveLocalBetType($recentResults, ! $isInLossCoverWindow); if ($betType !== 'pass') { if ($isInLossCoverWindow) { // 本地兜底策略命中买单活动时,同样优先按百家乐最高限额下注。 $amount = min($bettableGold, $maxBet); $reason = '买单活动进行中,采用本地最高限额下注兜底策略。'; } else { $percent = rand(2, 5) / 100.0; $amount = (int) round($bettableGold * $percent); $amount = max($minBet, min($amount, $maxBet)); } } } // 记录 AI 小班长本次决策日志 $labelMap = ['big' => '大', 'small' => '小', 'triple' => '豹子', 'pass' => '观望']; Log::channel('daily')->info('AI小班长百家乐决策', [ 'round_id' => $round->id, 'decision_source' => $decisionSource === 'ai' ? 'AI接口策略' : '本地路单兜底', 'action' => $labelMap[$betType] ?? $betType, 'bet_amount' => $amount, 'reason' => $reason, 'roadmap_20' => implode('→', array_map(fn (string $r) => $labelMap[$r] ?? $r, array_reverse($recentResults))), ]); if ($betType === 'pass') { // 观望时仅广播,不执行真正的金币扣除下注逻辑 $this->broadcastPassMessage($user, $round->room_id ?? 1, $reason); return; } // 买单活动期间允许为本次高额下注从银行调拨;非活动期间只检查当前手上金币是否够本次下注。 $isReadyToSpend = $isInLossCoverWindow ? $aiFinance->prepareAllInSpend($user, $amount) : $aiFinance->prepareSpend($user, $amount); if (! $isReadyToSpend) { return; } // 二次校验,防止大模型接口调用太慢导致下注时该局已关闭 $round->refresh(); if (! $round->isBettingOpen()) { Log::channel('daily')->warning("AI小班长百家乐下注拦截:由于AI接口思考耗时,这局#{$round->id}投注已截止。"); return; } // 8. 执行下注 (同 BaccaratController::bet 逻辑) DB::transaction(function () use ($user, $round, $betType, $amount, $currency, $lossCoverEvent, $lossCoverService) { $lockedUser = User::query()->lockForUpdate()->find($user->id); if (! $lockedUser || (int) ($lockedUser->jjb ?? 0) < $amount) { return; } // 幂等:同一局只能下一注 $existing = BaccaratBet::query() ->where('round_id', $round->id) ->where('user_id', $lockedUser->id) ->lockForUpdate() ->exists(); if ($existing) { return; } // 扣除金币 $currency->change( $lockedUser, 'gold', -$amount, CurrencySource::BACCARAT_BET, "AI小班长百家乐 #{$round->id} 押 ".match ($betType) { 'big' => '大', 'small' => '小', default => '豹子' }, ); // 写入下注记录 $bet = BaccaratBet::create([ 'round_id' => $round->id, 'user_id' => $lockedUser->id, 'loss_cover_event_id' => $lossCoverEvent?->id, 'bet_type' => $betType, 'amount' => $amount, 'status' => 'pending', ]); // 命中买单活动的下注需要登记到活动聚合记录里,确保后续能正确补偿返还。 if ($lossCoverEvent) { $lossCoverService->registerBet($bet); } // 更新局次汇总统计 $field = 'total_bet_'.$betType; $countField = 'bet_count_'.$betType; $round->increment($field, $amount); $round->increment($countField); $round->increment('bet_count'); // 广播各选项的最新押注人数(让前台看到 AI 下注的人数增长) event(new \App\Events\BaccaratPoolUpdated($round)); }); // 下注完成后,若手上金币仍高于 100 万,则把超出的部分继续回存银行。 $aiFinance->bankExcessGold($user); // 下注成功后,在聊天室发送一条普通聊天消息告知大家 $this->broadcastBetMessage($user, $round->room_id ?? 1, $betType, $amount, $decisionSource); } /** * 广播 AI小班长的观望文案到聊天室 * * @param User $user AI小班长用户 * @param int $roomId 聊天室 ID * @param string $reason 观望理由 */ private function broadcastPassMessage(User $user, int $roomId, string $reason): void { if (empty($reason)) { $reason = '风大雨大,保本最大,这把我决定观望一下!'; } $chatState = app(ChatStateService::class); $content = "🌟 🎲 【百家乐】 {$user->username} 本局选择挂机观望:✨
[🤖 策略分析] {$reason}"; $msg = [ 'id' => $chatState->nextMessageId($roomId), 'room_id' => $roomId, 'from_user' => '系统传音', 'to_user' => '大家', 'content' => $content, 'is_secret' => false, 'font_color' => '#d97706', 'action' => '', 'sent_at' => now()->toDateTimeString(), ]; $chatState->pushMessage($roomId, $msg); broadcast(new MessageSent($roomId, $msg)); SaveMessageJob::dispatch($msg); } /** * 广播 AI小班长本次下注情况到聊天室 * * 以普通聊天消息形式发送,发送对象为大家。 * * @param User $user AI小班长用户对象 * @param int $roomId 目标房间 ID * @param string $betType 下注方向:big|small|triple * @param int $amount 下注金额 * @param string $decisionSource 决策来源:ai | local */ private function broadcastBetMessage( User $user, int $roomId, string $betType, int $amount, string $decisionSource, ): void { $chatState = app(ChatStateService::class); $labelMap = ['big' => '大', 'small' => '小', 'triple' => '豹子']; $label = $labelMap[$betType] ?? $betType; $sourceText = $decisionSource === 'ai' ? '🤖 经过深度算法预测,本局我看好:' : '📊 观察了下最近的路单,这把我觉得是:'; $content = "🌟 🎲 【百家乐】 {$user->username} 已下注:{$label} (".number_format($amount)." 金币)
{$sourceText} {$label}!"; $msg = [ 'id' => $chatState->nextMessageId($roomId), 'room_id' => $roomId, 'from_user' => '系统传音', 'to_user' => '大家', 'content' => $content, 'is_secret' => false, 'font_color' => '#8b5cf6', 'action' => '动态播报', 'sent_at' => now()->toDateTimeString(), ]; $chatState->pushMessage($roomId, $msg); broadcast(new MessageSent($roomId, $msg)); SaveMessageJob::dispatch($msg); } /** * 生成本地路单兜底下注方向。 * * @param array $recentResults 最近已结算局次结果 * @param bool $allowPass 是否允许返回观望 */ private function resolveLocalBetType(array $recentResults, bool $allowPass): string { $bigCount = count(array_filter($recentResults, fn (string $result) => $result === 'big')); $smallCount = count(array_filter($recentResults, fn (string $result) => $result === 'small')); // 默认保留少量押豹子概率,维持 AI 小班长原本的下注风格。 if (rand(1, 100) <= 10) { return 'triple'; } if ($bigCount > $smallCount) { return rand(1, 100) <= 70 ? 'big' : 'small'; } if ($smallCount > $bigCount) { return rand(1, 100) <= 70 ? 'small' : 'big'; } // 只有非买单活动时才允许空仓;活动期间必须至少押大或押小。 if ($allowPass && rand(0, 10) === 0) { return 'pass'; } return rand(0, 1) === 0 ? 'big' : 'small'; } /** * AI 资金管理逻辑:优先领取补偿,再按“超过 100 万才存款”的规则整理持仓。 */ private function manageFinances(User $user, AiFinanceService $aiFinance): void { // 1. 检查是否有“买单”活动补偿可领取 (jjb 较低时优先领取) $lossCoverService = app(\App\Services\BaccaratLossCoverService::class); $pendingEvents = \App\Models\BaccaratLossCoverEvent::where('status', 'claimable')->get(); foreach ($pendingEvents as $event) { $record = \App\Models\BaccaratLossCoverRecord::where('event_id', $event->id) ->where('user_id', $user->id) ->where('claim_status', 'pending') ->first(); if ($record && $record->compensation_amount > 0) { $lossCoverService->claim($event, $user); Log::info("AI小班长自动领取活动补偿: Event #{$event->id}, Amount: {$record->compensation_amount}"); } } $aiFinance->rebalanceHoldings($user); } }