feat: 实现 AI 钓鱼与百家乐游戏的参与逻辑,并支持后台面板配置开关
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:AI小班长自动参与百家乐下注
|
||||
*
|
||||
* 在每局百家乐开启时延迟调度执行:
|
||||
* 1. 检查是否存在连输休息惩罚(1小时)
|
||||
* 2. 检查可用金币,确保留存底金
|
||||
* 3. 获取近期路单进行简单决策
|
||||
* 4. 提交下注
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Enums\CurrencySource;
|
||||
use App\Models\BaccaratBet;
|
||||
use App\Models\BaccaratRound;
|
||||
use App\Models\GameConfig;
|
||||
use App\Models\Sysparam;
|
||||
use App\Models\User;
|
||||
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\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. 决策逻辑:简单分析近期路单
|
||||
// 取最近 10 局
|
||||
$recentResults = BaccaratRound::query()
|
||||
->where('status', 'settled')
|
||||
->orderByDesc('id')
|
||||
->limit(10)
|
||||
->pluck('result')
|
||||
->toArray();
|
||||
|
||||
$bigCount = count(array_filter($recentResults, fn ($r) => $r === 'big'));
|
||||
$smallCount = count(array_filter($recentResults, fn ($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';
|
||||
}
|
||||
|
||||
// 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;
|
||||
$round->increment($field, $amount);
|
||||
$round->increment('bet_count');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:AI小班长钓鱼动作异步任务
|
||||
*
|
||||
* 模拟 AI 抛竿等待一定时间后收竿的过程。
|
||||
* 收竿时自动调用 FishingService 结算结果并播报。
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Services\FishingService;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class AiFishingJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* @param User $aiUser AI小班长
|
||||
* @param int $roomId 此事件广播的房间ID
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly User $aiUser,
|
||||
public readonly int $roomId,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 任务执行
|
||||
*/
|
||||
public function handle(FishingService $fishingService): void
|
||||
{
|
||||
// 通过业务服务计算钓鱼结果并广播,标记 isAi 为 true
|
||||
$fishingService->processCatch($this->aiUser, $this->roomId, true);
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ use App\Services\UserCurrencyService;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Queue\Queueable;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
|
||||
class CloseBaccaratRoundJob implements ShouldQueue
|
||||
{
|
||||
@@ -103,6 +104,10 @@ class CloseBaccaratRoundJob implements ShouldQueue
|
||||
$bet->update(['status' => 'lost', 'payout' => 0]);
|
||||
$losers[] = "{$username}-{$bet->amount}";
|
||||
|
||||
if ($username === 'AI小班长') {
|
||||
$this->handleAiLoseStreak();
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -121,9 +126,17 @@ class CloseBaccaratRoundJob implements ShouldQueue
|
||||
);
|
||||
$totalPayout += $payout;
|
||||
$winners[] = "{$username}+".number_format($payout);
|
||||
|
||||
if ($username === 'AI小班长') {
|
||||
Redis::del('ai_baccarat_lose_streak'); // 赢了清空连输
|
||||
}
|
||||
} else {
|
||||
$bet->update(['status' => 'lost', 'payout' => 0]);
|
||||
$losers[] = "{$username}-".number_format($bet->amount);
|
||||
|
||||
if ($username === 'AI小班长') {
|
||||
$this->handleAiLoseStreak();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -149,6 +162,18 @@ class CloseBaccaratRoundJob implements ShouldQueue
|
||||
$this->pushResultMessage($round, $chatState, $winners, $losers);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 AI 小班长连输逻辑
|
||||
*/
|
||||
private function handleAiLoseStreak(): void
|
||||
{
|
||||
$streak = Redis::incr('ai_baccarat_lose_streak');
|
||||
if ($streak >= 3) {
|
||||
Redis::setex('ai_baccarat_timeout', 3600, 'timeout'); // 连输三次,停赛1小时
|
||||
Redis::del('ai_baccarat_lose_streak');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 向公屏发送开奖结果系统消息(含各用户输赢情况)。
|
||||
*
|
||||
|
||||
@@ -89,6 +89,13 @@ class OpenBaccaratRoundJob implements ShouldQueue
|
||||
broadcast(new MessageSent(1, $msg));
|
||||
SaveMessageJob::dispatch($msg);
|
||||
|
||||
// 如果允许 AI 参与,延迟一定时间派发 AI 下注任务
|
||||
$baccaratEnabled = \App\Models\Sysparam::getValue('chatbot_baccarat_enabled', '0') === '1';
|
||||
if (\App\Models\Sysparam::getValue('chatbot_enabled', '0') === '1' && $baccaratEnabled) {
|
||||
$aiDelay = rand(10, min(40, max(10, $betSeconds - 5))); // 随机在 10 ~ (倒数前5秒) 之间下注
|
||||
\App\Jobs\AiBaccaratBetJob::dispatch($round)->delay(now()->addSeconds($aiDelay));
|
||||
}
|
||||
|
||||
// 在下注截止时安排结算任务
|
||||
CloseBaccaratRoundJob::dispatch($round)->delay($closesAt);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user