Files
chatroom/app/Jobs/AiBaccaratBetJob.php
T

224 lines
7.8 KiB
PHP

<?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; // 资金不足以支撑最小下注
}
// 下注金额:可用余额的 2% ~ 5%,并在 min_bet 和 max_bet 之间
$percent = rand(2, 5) / 100.0;
$amount = (int) round($availableGold * $percent);
$amount = max($minBet, min($amount, $maxBet));
// 如果依然大于实际 jjb (保险兜底),则放弃
if ($amount > $user->jjb) {
return;
}
// 4. 获取近期路单(最多取 20 局)
$recentResults = BaccaratRound::query()
->where('status', 'settled')
->orderByDesc('id')
->limit(20)
->pluck('result')
->toArray();
// 5. 优先调用 AI 接口预测下注方向
$predictionService = app(BaccaratPredictionService::class);
$aiPrediction = $predictionService->predict($recentResults);
$decisionSource = 'ai';
$betType = $aiPrediction;
// AI 不可用时回退本地路单决策(保底逻辑)
if ($betType === null) {
$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) {
// 大偏热:70% 概率顺势买大,30% 逆势买小
$betType = rand(1, 100) <= 70 ? 'big' : 'small';
} elseif ($smallCount > $bigCount) {
$betType = rand(1, 100) <= 70 ? 'small' : 'big';
} else {
$betType = rand(0, 1) ? 'big' : 'small';
}
}
// 记录 AI 小班长本次决策日志
$labelMap = ['big' => '大', 'small' => '小', 'triple' => '豹子'];
Log::channel('daily')->info('AI小班长百家乐决策', [
'round_id' => $round->id,
'decision_source' => $decisionSource === 'ai' ? 'AI接口预测' : '本地路单兜底',
'ai_prediction' => $aiPrediction ? $labelMap[$aiPrediction] : 'null(不可用)',
'final_bet' => $labelMap[$betType] ?? $betType,
'bet_amount' => $amount,
'roadmap_20' => implode('→', array_map(fn (string $r) => $labelMap[$r] ?? $r, array_reverse($recentResults))),
]);
// 5. 执行下注 (同 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 $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分析' : '📊路单统计';
$content = "{$sourceTag} 小班长投了 {$amount} 金币,压【{$betLabel}】,大家加油!🎲";
$msg = [
'id' => $chatState->nextMessageId($roomId),
'room_id' => $roomId,
'from_user' => $user->username,
'to_user' => '大家',
'content' => $content,
'is_secret' => false,
'font_color' => null,
'action' => '说',
'sent_at' => now()->toDateTimeString(),
];
$chatState->pushMessage($roomId, $msg);
broadcast(new MessageSent($roomId, $msg));
SaveMessageJob::dispatch($msg);
}
}