Fix horse race seed pool payouts
This commit is contained in:
@@ -51,6 +51,7 @@ class HorseRaceController extends Controller
|
||||
// 计算各马匹当前注额
|
||||
$config = GameConfig::forGame('horse_racing')?->params ?? [];
|
||||
$houseTake = (int) ($config['house_take_percent'] ?? 5);
|
||||
$seedPool = (int) ($config['seed_pool'] ?? 0);
|
||||
|
||||
$horsePools = HorseBet::query()
|
||||
->where('race_id', $race->id)
|
||||
@@ -59,13 +60,13 @@ class HorseRaceController extends Controller
|
||||
->pluck('pool', 'horse_id')
|
||||
->toArray();
|
||||
|
||||
$oddsMap = HorseRace::calcOdds($horsePools, $houseTake, $seedPool);
|
||||
|
||||
// 计算实时赔率
|
||||
$horses = $race->horses ?? [];
|
||||
$horsesWithBets = array_map(function ($horse) use ($horsePools, $houseTake) {
|
||||
$horsesWithBets = array_map(function ($horse) use ($horsePools, $oddsMap) {
|
||||
$horsePool = (int) ($horsePools[$horse['id']] ?? 0);
|
||||
$totalPool = array_sum(array_values($horsePools));
|
||||
$netPool = $totalPool * (1 - $houseTake / 100);
|
||||
$odds = $horsePool > 0 ? round($netPool / $horsePool, 2) : null;
|
||||
$odds = $horsePool > 0 ? ($oddsMap[$horse['id']] ?? null) : null;
|
||||
|
||||
return [
|
||||
'id' => $horse['id'],
|
||||
|
||||
@@ -70,6 +70,7 @@ class CloseHorseRaceJob implements ShouldQueue
|
||||
|
||||
$config = GameConfig::forGame('horse_racing')?->params ?? [];
|
||||
$houseTake = (int) ($config['house_take_percent'] ?? 5);
|
||||
$seedPool = (int) ($config['seed_pool'] ?? 0);
|
||||
$winnerId = (int) $race->winner_horse_id;
|
||||
|
||||
// 按马匹统计各匹下注金额
|
||||
@@ -82,7 +83,7 @@ class CloseHorseRaceJob implements ShouldQueue
|
||||
|
||||
$totalPool = array_sum($horsePools);
|
||||
$winnerPool = (int) ($horsePools[$winnerId] ?? 0);
|
||||
$netPool = (int) ($totalPool * (1 - $houseTake / 100));
|
||||
$distributablePool = HorseRace::calcDistributablePool($horsePools, $houseTake, $seedPool, $winnerPool);
|
||||
|
||||
// 结算:遍历所有下注记录
|
||||
$bets = HorseBet::query()
|
||||
@@ -93,7 +94,7 @@ class CloseHorseRaceJob implements ShouldQueue
|
||||
|
||||
$totalPayout = 0;
|
||||
|
||||
DB::transaction(function () use ($bets, $winnerId, $netPool, $winnerPool, $currency, &$totalPayout) {
|
||||
DB::transaction(function () use ($bets, $winnerId, $distributablePool, $winnerPool, $currency, &$totalPayout) {
|
||||
foreach ($bets as $bet) {
|
||||
if ((int) $bet->horse_id !== $winnerId) {
|
||||
// 未中奖(本金已在下注时扣除)
|
||||
@@ -104,7 +105,7 @@ class CloseHorseRaceJob implements ShouldQueue
|
||||
|
||||
// 中奖:按注额比例分配净注池
|
||||
if ($winnerPool > 0) {
|
||||
$payout = (int) round($netPool * ($bet->amount / $winnerPool));
|
||||
$payout = (int) round($distributablePool * ($bet->amount / $winnerPool));
|
||||
} else {
|
||||
$payout = 0;
|
||||
}
|
||||
|
||||
@@ -114,33 +114,43 @@ class HorseRace extends Model
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据注池计算各马匹实时赔率(彩池制,扣除庄家抽水后按比例分配)。
|
||||
* 计算本场可派奖总池。
|
||||
*
|
||||
* @param int $horseBetAmounts 各马匹的注额数组 [horse_id => amount]
|
||||
* @param int $housePercent 庄家抽水百分比
|
||||
* 可派奖池 = 系统初始化资金池 + 玩家总下注 - 抽水,
|
||||
* 且至少不低于中奖马匹总下注,避免出现“押中反亏”的体验。
|
||||
*
|
||||
* @param array<int, int|float> $horseBetAmounts
|
||||
*/
|
||||
public static function calcDistributablePool(array $horseBetAmounts, int $housePercent, int $seedPool = 0, int $winnerPool = 0): float
|
||||
{
|
||||
$totalPool = array_sum($horseBetAmounts);
|
||||
$netPlayerPool = $totalPool * (1 - $housePercent / 100);
|
||||
|
||||
return max($netPlayerPool + max(0, $seedPool), $winnerPool);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据注池计算各马匹实时赔率(含系统初始化资金池)。
|
||||
*
|
||||
* @param array<int, int|float> $horseBetAmounts 各马匹的注额数组 [horse_id => amount]
|
||||
* @return array<int, float> horse_id => 赔率(含本金)
|
||||
*/
|
||||
public static function calcOdds(array $horseBetAmounts, int $housePercent): array
|
||||
public static function calcOdds(array $horseBetAmounts, int $housePercent, int $seedPool = 0): array
|
||||
{
|
||||
$totalPool = array_sum($horseBetAmounts);
|
||||
|
||||
if ($totalPool <= 0) {
|
||||
// 尚无下注,返回等额赔率
|
||||
$count = count($horseBetAmounts);
|
||||
|
||||
return array_map(fn () => 1.0, $horseBetAmounts);
|
||||
}
|
||||
|
||||
$netPool = $totalPool * (1 - $housePercent / 100);
|
||||
$distributablePool = static::calcDistributablePool($horseBetAmounts, $housePercent, $seedPool);
|
||||
$odds = [];
|
||||
|
||||
foreach ($horseBetAmounts as $horseId => $amount) {
|
||||
if ($amount <= 0) {
|
||||
// 无人押注的马,赔率设为理论最大值
|
||||
$odds[$horseId] = round($netPool, 2);
|
||||
$odds[$horseId] = round($distributablePool, 2);
|
||||
} else {
|
||||
// 赔率 = 净注池 / 该马注额(含本金返还)
|
||||
$odds[$horseId] = round($netPool / $amount, 2);
|
||||
$odds[$horseId] = round($distributablePool / $amount, 2);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -97,6 +97,7 @@ class GameConfigSeeder extends Seeder
|
||||
'horse_count' => 4, // 参赛马匹数量
|
||||
'min_bet' => 100, // 最低押注
|
||||
'max_bet' => 100000, // 最高押注
|
||||
'seed_pool' => 10000, // 系统初始化资金池
|
||||
'house_take_percent' => 5, // 庄家抽水(%)
|
||||
],
|
||||
],
|
||||
|
||||
@@ -93,10 +93,12 @@
|
||||
@php
|
||||
$params = $game->params ?? [];
|
||||
$labels = gameParamLabels($game->game_key);
|
||||
$paramKeys = array_values(array_unique(array_merge(array_keys($labels), array_keys($params))));
|
||||
@endphp
|
||||
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
@foreach ($params as $paramKey => $paramValue)
|
||||
@foreach ($paramKeys as $paramKey)
|
||||
@php $paramValue = $params[$paramKey] ?? ($labels[$paramKey]['default'] ?? '') @endphp
|
||||
@php $meta = $labels[$paramKey] ?? ['label' => $paramKey, 'type' => 'number', 'unit' => ''] @endphp
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-gray-600 mb-1">
|
||||
@@ -618,6 +620,7 @@
|
||||
'horse_count' => ['label' => '参赛马匹数', 'type' => 'number', 'unit' => '匹', 'min' => 2, 'max' => 8],
|
||||
'min_bet' => ['label' => '最低押注', 'type' => 'number', 'unit' => '金币', 'min' => 1],
|
||||
'max_bet' => ['label' => '最高押注', 'type' => 'number', 'unit' => '金币', 'min' => 1],
|
||||
'seed_pool' => ['label' => '系统初始化资金池', 'type' => 'number', 'unit' => '金币', 'min' => 0],
|
||||
'house_take_percent' => [
|
||||
'label' => '庄家抽水',
|
||||
'type' => 'number',
|
||||
|
||||
@@ -136,7 +136,8 @@
|
||||
return [
|
||||
'投注范围 ' . number_format($p['min_bet'] ?? 100) . '~' . number_format($p['max_bet'] ?? 100000) . ' 金币',
|
||||
'每 ' . ($p['interval_minutes'] ?? 30) . ' 分钟一场,投注窗口 ' . ($p['bet_window_seconds'] ?? 90) . ' 秒',
|
||||
'平台抽成 ' . ($p['house_take_percent'] ?? 5) . '%,剩余按得票比例派奖',
|
||||
'系统初始化资金池 ' . number_format($p['seed_pool'] ?? 0) . ' 金币,连同玩家注池一起派奖',
|
||||
'平台抽成 ' . ($p['house_take_percent'] ?? 5) . '%,中奖时按占比瓜分派奖池',
|
||||
];
|
||||
},
|
||||
],
|
||||
|
||||
@@ -2,12 +2,15 @@
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Jobs\CloseHorseRaceJob;
|
||||
use App\Models\GameConfig;
|
||||
use App\Models\HorseBet;
|
||||
use App\Models\HorseRace;
|
||||
use App\Models\User;
|
||||
use App\Services\ChatStateService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Tests\TestCase;
|
||||
|
||||
class HorseRaceControllerTest extends TestCase
|
||||
@@ -182,4 +185,66 @@ class HorseRaceControllerTest extends TestCase
|
||||
$response->assertJsonStructure(['history']);
|
||||
$this->assertCount(1, $response->json('history'));
|
||||
}
|
||||
|
||||
public function test_single_winner_receives_seed_pool_and_does_not_lose_principal(): void
|
||||
{
|
||||
Event::fake();
|
||||
Queue::fake();
|
||||
|
||||
GameConfig::updateOrCreate(
|
||||
['game_key' => 'horse_racing'],
|
||||
[
|
||||
'name' => 'Horse Racing',
|
||||
'icon' => 'horse',
|
||||
'description' => 'Horse Racing Game',
|
||||
'enabled' => true,
|
||||
'params' => [
|
||||
'min_bet' => 100,
|
||||
'max_bet' => 100000,
|
||||
'house_take_percent' => 5,
|
||||
'seed_pool' => 10,
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
/** @var \App\Models\User $user */
|
||||
$user = User::factory()->create(['jjb' => 500]);
|
||||
|
||||
$race = HorseRace::create([
|
||||
'status' => 'running',
|
||||
'bet_opens_at' => now()->subMinutes(2),
|
||||
'bet_closes_at' => now()->subMinute(),
|
||||
'race_starts_at' => now()->subSeconds(30),
|
||||
'winner_horse_id' => 1,
|
||||
'horses' => [
|
||||
['id' => 1, 'name' => 'Horse A', 'emoji' => '🐎'],
|
||||
['id' => 2, 'name' => 'Horse B', 'emoji' => '🏇'],
|
||||
],
|
||||
]);
|
||||
|
||||
HorseBet::create([
|
||||
'race_id' => $race->id,
|
||||
'user_id' => $user->id,
|
||||
'horse_id' => 1,
|
||||
'amount' => 100,
|
||||
'status' => 'pending',
|
||||
]);
|
||||
|
||||
$user->decrement('jjb', 100);
|
||||
|
||||
$chatState = $this->createMock(ChatStateService::class);
|
||||
$chatState->method('nextMessageId')->willReturn(1);
|
||||
$chatState->method('pushMessage');
|
||||
|
||||
app()->instance(ChatStateService::class, $chatState);
|
||||
|
||||
(new CloseHorseRaceJob($race))->handle(app('App\Services\UserCurrencyService'), $chatState);
|
||||
|
||||
$bet = HorseBet::query()->first();
|
||||
|
||||
$this->assertNotNull($bet);
|
||||
$this->assertSame('won', $bet->status);
|
||||
$this->assertSame(105, $bet->payout);
|
||||
$this->assertSame(505, $user->fresh()->jjb);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user