Files
chatroom/app/Jobs/AiBaccaratBetJob.php

311 lines
11 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 文件功能AI小班长自动参与百家乐下注
*
* 在每局百家乐开启时延迟调度执行:
* 1. 检查是否存在连输休息惩罚1小时
* 2. 检查可用金币,确保留存底金
* 3. 调用 AI 接口预测路单走势决定下注方向AI 不可用时回退本地决策)
* 4. 提交下注
*
* @author ChatRoom Laravel
*
* @version 1.0.0
*/
namespace App\Jobs;
use App\Enums\CurrencySource;
use App\Events\MessageSent;
use App\Models\BaccaratBet;
use App\Models\BaccaratRound;
use App\Models\GameConfig;
use App\Models\Sysparam;
use App\Models\User;
use App\Services\BaccaratPredictionService;
use App\Services\ChatStateService;
use App\Services\UserCurrencyService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;
class AiBaccaratBetJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(
public readonly BaccaratRound $round,
) {}
public function handle(UserCurrencyService $currency): void
{
// 1. 检查总开关与游戏开关
if (Sysparam::getValue('chatbot_enabled', '0') !== '1' || ! GameConfig::isEnabled('baccarat')) {
return;
}
$round = $this->round->fresh();
if (! $round || ! $round->isBettingOpen()) {
return;
}
$user = User::where('username', 'AI小班长')->first();
if (! $user) {
return;
}
// 2. 检查连输惩罚超时
if (Redis::exists('ai_baccarat_timeout')) {
return; // 还在禁赛期
}
// 3. 检查余额与限额
$config = GameConfig::forGame('baccarat')?->params ?? [];
$minBet = (int) ($config['min_bet'] ?? 100);
$maxBet = (int) ($config['max_bet'] ?? 50000);
// 至少保留 2000 金币底仓
$availableGold = ($user->jjb ?? 0) - 2000;
if ($availableGold < $minBet) {
return; // 资金不足以支撑最小下注
}
// 4. 获取近期路单和 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();
// 5. 调用 AI 接口出具统筹策略
$predictionService = app(BaccaratPredictionService::class);
$context = [
'recent_results' => $recentResults,
'available_gold' => $availableGold,
'historical_bets' => $historicalBets,
];
$aiPrediction = $predictionService->predict($context);
$decisionSource = 'ai';
$betType = 'pass';
$amount = 0;
$reason = '';
if ($aiPrediction) {
$betType = $aiPrediction['action'];
$percent = $aiPrediction['percentage'];
$reason = $aiPrediction['reason'];
// 限定单局最高下注不超过可用金额的 5% 以防止 AI "乱梭哈" 破产
$percent = min(5, max(0, $percent));
if ($betType !== 'pass') {
$amount = (int) round($availableGold * ($percent / 100.0));
$amount = max($minBet, min($amount, $maxBet));
if ($amount > $user->jjb) {
$amount = $user->jjb;
}
}
} else {
// AI 不可用时回退本地路单决策(保底逻辑)
$decisionSource = 'local';
$bigCount = count(array_filter($recentResults, fn (string $r) => $r === 'big'));
$smallCount = count(array_filter($recentResults, fn (string $r) => $r === 'small'));
$strategy = rand(1, 100);
if ($strategy <= 10) {
$betType = 'triple'; // 10% 概率博豹子
} elseif ($bigCount > $smallCount) {
$betType = rand(1, 100) <= 70 ? 'big' : 'small';
} elseif ($smallCount > $bigCount) {
$betType = rand(1, 100) <= 70 ? 'small' : 'big';
} else {
$betType = rand(0, 10) === 0 ? 'pass' : (rand(0, 1) ? 'big' : 'small');
}
if ($betType !== 'pass') {
$percent = rand(2, 5) / 100.0;
$amount = (int) round($availableGold * $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;
}
// 二次校验,防止大模型接口调用太慢导致下注时该局已关闭
$round->refresh();
if (! $round->isBettingOpen()) {
Log::channel('daily')->warning("AI小班长百家乐下注拦截由于AI接口思考耗时这局#{$round->id}投注已截止。");
return;
}
// 6. 执行下注 (同 BaccaratController::bet 逻辑)
DB::transaction(function () use ($user, $round, $betType, $amount, $currency) {
// 幂等:同一局只能下一注
$existing = BaccaratBet::query()
->where('round_id', $round->id)
->where('user_id', $user->id)
->lockForUpdate()
->exists();
if ($existing) {
return;
}
// 扣除金币
$currency->change(
$user,
'gold',
-$amount,
CurrencySource::BACCARAT_BET,
"AI小班长百家乐 #{$round->id}".match ($betType) {
'big' => '大', 'small' => '小', default => '豹子'
},
);
// 写入下注记录
BaccaratBet::create([
'round_id' => $round->id,
'user_id' => $user->id,
'bet_type' => $betType,
'amount' => $amount,
'status' => 'pending',
]);
// 更新局次汇总统计
$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));
});
// 下注成功后,在聊天室发送一条普通聊天消息告知大家
$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 = "🌟 🎲 【百家乐】 <b>{$user->username}</b> 本局选择挂机观望:✨ <br/><span style='color:#666;'>[🤖 策略分析] {$reason}</span>";
$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' => '豹子'];
$betLabel = $labelMap[$betType] ?? $betType;
$sourceTag = $decisionSource === 'ai' ? '🤖 AI分析' : '📊路单统计';
$formattedAmount = number_format($amount);
// 格式:🌟 🎲 【百家乐】 娜姐 押注了 119 金币(大)!✨ [🤖 AI分析]
$content = "🌟 🎲 【百家乐】 <b>{$user->username}</b> 押注了 <b>{$formattedAmount}</b> 金币({$betLabel})!✨ [{$sourceTag}]";
$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);
}
}