From bd97ed0b73b78c8129eaa704b3f37f54e57d3557 Mon Sep 17 00:00:00 2001 From: lkddi Date: Sun, 19 Apr 2026 12:36:23 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=20ai=E5=B0=8F=E7=8F=AD?= =?UTF-8?q?=E9=95=BF=E7=99=BE=E5=AE=B6=E4=B9=90=E6=8A=BC=E6=B3=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Jobs/AiBaccaratBetJob.php | 54 +++++++++++++---- app/Services/BaccaratPredictionService.php | 7 +++ tests/Feature/AiBaccaratBetJobTest.php | 70 ++++++++++++++++++++++ 3 files changed, 118 insertions(+), 13 deletions(-) diff --git a/app/Jobs/AiBaccaratBetJob.php b/app/Jobs/AiBaccaratBetJob.php index 8a5a0c6..30b3586 100644 --- a/app/Jobs/AiBaccaratBetJob.php +++ b/app/Jobs/AiBaccaratBetJob.php @@ -144,6 +144,13 @@ class AiBaccaratBetJob implements ShouldQueue $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) { // 买单活动进行中且金币足够时,直接按百家乐单局最高限额下注。 @@ -162,19 +169,8 @@ class AiBaccaratBetJob implements ShouldQueue } 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'); - } + // 买单活动期间,本地兜底策略同样不能返回观望。 + $betType = $this->resolveLocalBetType($recentResults, ! $isInLossCoverWindow); if ($betType !== 'pass') { if ($isInLossCoverWindow) { @@ -359,6 +355,38 @@ class AiBaccaratBetJob implements ShouldQueue 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 万才存款”的规则整理持仓。 */ diff --git a/app/Services/BaccaratPredictionService.php b/app/Services/BaccaratPredictionService.php index a678acf..138c81a 100644 --- a/app/Services/BaccaratPredictionService.php +++ b/app/Services/BaccaratPredictionService.php @@ -113,6 +113,7 @@ class BaccaratPredictionService $recentResults = $context['recent_results'] ?? []; $availableGold = $context['available_gold'] ?? 0; $history = $context['historical_bets'] ?? []; + $lossCoverActive = (bool) ($context['loss_cover_active'] ?? false); // 将英文 key 转为中文展示,方便 AI 理解 $labelMap = ['big' => '大', 'small' => '小', 'triple' => '豹子']; @@ -137,11 +138,16 @@ class BaccaratPredictionService } } + $lossCoverText = $lossCoverActive + ? '当前处于“你玩游戏我买单”活动时间窗口内。你本局必须参与下注,不能返回 pass;金额已由系统固定为单局最高限额,你只需要判断下注方向。' + : '当前不在“你玩游戏我买单”活动时间窗口内。你可以按常规风控策略决定是否观望。'; + return << 200000, ]); } + + /** + * 买单活动时间窗口命中时,即使 AI 返回观望,系统也会强制切回本地策略完成下注。 + */ + public function test_ai_still_places_bet_when_loss_cover_time_window_is_active_and_ai_returns_pass(): void + { + Event::fake(); + Queue::fake([SaveMessageJob::class]); + + $this->mock(ChatStateService::class, function (MockInterface $mock): void { + $mock->shouldReceive('nextMessageId')->andReturn(1); + $mock->shouldReceive('pushMessage')->zeroOrMoreTimes(); + $mock->shouldReceive('getAllActiveRoomIds')->andReturn([1]); + }); + + $this->mock(BaccaratPredictionService::class, function (MockInterface $mock): void { + $mock->shouldReceive('predict') + ->once() + ->andReturn([ + 'action' => 'pass', + 'percentage' => 0, + 'reason' => '常规风控建议先观望', + ]); + }); + + $aiUser = User::factory()->create([ + 'username' => 'AI小班长', + 'jjb' => 600000, + 'bank_jjb' => 0, + ]); + + $event = BaccaratLossCoverEvent::factory()->create([ + 'status' => 'active', + 'starts_at' => now()->subMinutes(2), + 'ends_at' => now()->addMinutes(10), + ]); + + $round = BaccaratRound::forceCreate([ + 'status' => 'betting', + 'bet_opens_at' => now()->subSeconds(10), + 'bet_closes_at' => now()->addMinute(), + 'total_bet_big' => 0, + 'total_bet_small' => 0, + 'total_bet_triple' => 0, + 'bet_count' => 0, + 'bet_count_big' => 0, + 'bet_count_small' => 0, + 'bet_count_triple' => 0, + 'total_payout' => 0, + ]); + + $job = new AiBaccaratBetJob($round); + $job->handle(app(\App\Services\UserCurrencyService::class), app(\App\Services\AiFinanceService::class)); + + $bet = \App\Models\BaccaratBet::query() + ->where('round_id', $round->id) + ->where('user_id', $aiUser->id) + ->first(); + + $this->assertNotNull($bet); + $this->assertContains($bet->bet_type, ['big', 'small', 'triple']); + $this->assertSame(50000, (int) $bet->amount); + $this->assertSame($event->id, $bet->loss_cover_event_id); + + $this->assertDatabaseHas('baccarat_loss_cover_records', [ + 'event_id' => $event->id, + 'user_id' => $aiUser->id, + 'total_bet_amount' => 50000, + ]); + } }