优化游玩方式

This commit is contained in:
2026-03-25 15:51:50 +08:00
parent 9b4104fc0e
commit 6b9fb0c26e
11 changed files with 96 additions and 37 deletions

View File

@@ -64,6 +64,8 @@
"player": "Player", "player": "Player",
"lotteryPoolConfig": "Lottery Pool Config", "lotteryPoolConfig": "Lottery Pool Config",
"drawType": "Draw Type", "drawType": "Draw Type",
"ante": "Ante",
"paidAmount": "Paid Amount",
"isBigWin": "Is Big Win", "isBigWin": "Is Big Win",
"winCoin": "Win Coin", "winCoin": "Win Coin",
"superWinCoin": "Super Win Coin", "superWinCoin": "Super Win Coin",

View File

@@ -64,6 +64,8 @@
"player": "玩家", "player": "玩家",
"lotteryPoolConfig": "彩金池配置", "lotteryPoolConfig": "彩金池配置",
"drawType": "抽奖类型", "drawType": "抽奖类型",
"ante": "注数",
"paidAmount": "付费金额",
"isBigWin": "是否中大奖", "isBigWin": "是否中大奖",
"winCoin": "赢取平台币", "winCoin": "赢取平台币",
"superWinCoin": "中大奖平台币", "superWinCoin": "中大奖平台币",

View File

@@ -199,6 +199,8 @@
useSlot: true useSlot: true
}, },
{ prop: 'lottery_type', label: 'page.table.drawType', width: 100, useSlot: true }, { prop: 'lottery_type', label: 'page.table.drawType', width: 100, useSlot: true },
{ prop: 'ante', label: 'page.table.ante', width: 80, align: 'center' },
{ prop: 'paid_amount', label: 'page.table.paidAmount', width: 110, align: 'center' },
{ prop: 'is_win', label: 'page.table.isBigWin', width: 100, useSlot: true }, { prop: 'is_win', label: 'page.table.isBigWin', width: 100, useSlot: true },
{ prop: 'win_coin', label: 'page.table.winCoin', width: 110 }, { prop: 'win_coin', label: 'page.table.winCoin', width: 110 },
{ prop: 'super_win_coin', label: 'page.table.superWinCoin', width: 120 }, { prop: 'super_win_coin', label: 'page.table.superWinCoin', width: 120 },

View File

@@ -162,21 +162,21 @@ class GameController extends BaseController
if ($direction !== null) { if ($direction !== null) {
$direction = (int) $direction; $direction = (int) $direction;
} }
$ante = $request->post('ante');
if ($ante !== null) {
$ante = (int) $ante;
}
if (!in_array($direction, [0, 1], true)) { if (!in_array($direction, [0, 1], true)) {
return $this->fail('direction must be 0 or 1', ReturnCode::PARAMS_ERROR); return $this->fail('direction must be 0 or 1', ReturnCode::PARAMS_ERROR);
} }
if (!is_int($ante) || $ante <= 0) {
return $this->fail('ante must be a positive integer', ReturnCode::PARAMS_ERROR);
}
$player = DicePlayer::find($userId); $player = DicePlayer::find($userId);
if (!$player) { if (!$player) {
return $this->fail('User not found', ReturnCode::NOT_FOUND); return $this->fail('User not found', ReturnCode::NOT_FOUND);
} }
$minEv = DiceRewardConfig::getCachedMinRealEv();
$minCoin = abs($minEv + 100);
$coin = (float) $player->coin;
if ($coin < $minCoin) {
$msg = ApiLang::translateParams('Balance %s is less than %s, cannot continue', [$coin, $minCoin], $request);
return $this->success([], $msg);
}
$lockName = 'play_start_' . $userId; $lockName = 'play_start_' . $userId;
$lockResult = Db::query('SELECT GET_LOCK(?, 30) as l', [$lockName]); $lockResult = Db::query('SELECT GET_LOCK(?, 30) as l', [$lockName]);
@@ -185,7 +185,7 @@ class GameController extends BaseController
} }
try { try {
$logic = new PlayStartLogic(); $logic = new PlayStartLogic();
$data = $logic->run($userId, (int)$direction); $data = $logic->run($userId, (int) $direction, $ante);
$lang = $request->header('lang', 'zh'); $lang = $request->header('lang', 'zh');
if (!is_string($lang) || $lang === '') { if (!is_string($lang) || $lang === '') {

View File

@@ -108,6 +108,7 @@ class GameLogic
'player_id' => $playerId, 'player_id' => $playerId,
'admin_id' => $adminId, 'admin_id' => $adminId,
'use_coins' => $cost, 'use_coins' => $cost,
'ante' => 1,
'total_ticket_count' => $addTotal, 'total_ticket_count' => $addTotal,
'paid_ticket_count' => $addPaid, 'paid_ticket_count' => $addPaid,
'free_ticket_count' => $addFree, 'free_ticket_count' => $addFree,

View File

@@ -8,6 +8,7 @@ use app\api\util\ApiLang;
use app\api\service\LotteryService; use app\api\service\LotteryService;
use app\dice\model\lottery_pool_config\DiceLotteryPoolConfig; use app\dice\model\lottery_pool_config\DiceLotteryPoolConfig;
use app\dice\model\play_record\DicePlayRecord; use app\dice\model\play_record\DicePlayRecord;
use app\dice\model\ante_config\DiceAnteConfig;
use app\dice\model\player\DicePlayer; use app\dice\model\player\DicePlayer;
use app\dice\model\player_ticket_record\DicePlayerTicketRecord; use app\dice\model\player_ticket_record\DicePlayerTicketRecord;
use app\dice\model\player_wallet_record\DicePlayerWalletRecord; use app\dice\model\player_wallet_record\DicePlayerWalletRecord;
@@ -34,8 +35,12 @@ class PlayStartLogic
/** 对局状态:超时/失败 */ /** 对局状态:超时/失败 */
public const RECORD_STATUS_TIMEOUT = 0; public const RECORD_STATUS_TIMEOUT = 0;
/** 开启对局最低余额 = |DiceRewardConfig 最小 real_ev + 100| */ /** 单注费用(对应原票价 100 */
private const MIN_COIN_EXTRA = 100; private const UNIT_COST = 100;
/** 免费抽奖注数缓存 key 前缀(用于强制下一局注数一致) */
private const FREE_ANTE_KEY_PREFIX = 'api:game:free_ante:';
/** 免费抽奖注数缓存过期(秒) */
private const FREE_ANTE_TTL = 86400 * 7;
/** 豹子号中大奖额外平台币(无 BIGWIN 配置时兜底) */ /** 豹子号中大奖额外平台币(无 BIGWIN 配置时兜底) */
private const SUPER_WIN_BONUS = 500; private const SUPER_WIN_BONUS = 500;
/** 可触发超级大奖的 grid_number5=全1 10=全2 15=全3 20=全4 25=全5 30=全6 */ /** 可触发超级大奖的 grid_number5=全1 10=全2 15=全3 20=全4 25=全5 30=全6 */
@@ -47,36 +52,60 @@ class PlayStartLogic
* 执行一局游戏 * 执行一局游戏
* @param int $playerId 玩家ID * @param int $playerId 玩家ID
* @param int $direction 方向 0=无/顺时针 1=中奖/逆时针(前端 direction * @param int $direction 方向 0=无/顺时针 1=中奖/逆时针(前端 direction
* @param int $ante 注数(必须在 DiceAnteConfig.mult 中存在)
* @return array 成功返回 DicePlayRecord 数据;余额不足时抛 ApiExceptionmessage 为约定文案 * @return array 成功返回 DicePlayRecord 数据;余额不足时抛 ApiExceptionmessage 为约定文案
*/ */
public function run(int $playerId, int $direction): array public function run(int $playerId, int $direction, int $ante): array
{ {
$player = DicePlayer::find($playerId); $player = DicePlayer::find($playerId);
if (!$player) { if (!$player) {
throw new ApiException('User not found'); throw new ApiException('User not found');
} }
$minEv = DiceRewardConfig::getCachedMinRealEv();
$minCoin = abs($minEv + self::MIN_COIN_EXTRA);
$coin = (float) $player->coin; $coin = (float) $player->coin;
if ($coin < $minCoin) { if ($ante <= 0) {
throw new ApiException(ApiLang::translateParams('当前玩家余额%s小于%s无法继续游戏', [$coin, $minCoin])); throw new ApiException('ante must be a positive integer');
} }
$paid = (int) ($player->paid_ticket_count ?? 0); // 注数合规校验ante 必须存在于 dice_ante_config.mult
$free = (int) ($player->free_ticket_count ?? 0); $anteConfigModel = new DiceAnteConfig();
if ($paid + $free <= 0) { $exists = $anteConfigModel->where('mult', $ante)->count();
throw new ApiException('Insufficient lottery tickets'); if ($exists <= 0) {
throw new ApiException('当前注数不合规,请选择正确的注数');
}
// 免费抽奖:不再使用抽奖券作为开始条件,仅用 free_ticket_count 表示“免费抽奖次数”
$freeCount = (int) ($player->free_ticket_count ?? 0);
$isFree = $freeCount > 0;
$ticketType = $isFree ? self::LOTTERY_TYPE_FREE : self::LOTTERY_TYPE_PAID;
// 若为免费抽奖:注数必须与上一次触发免费抽奖时的注数一致
if ($isFree) {
$requiredAnte = Cache::get(self::FREE_ANTE_KEY_PREFIX . $playerId);
if ($requiredAnte !== null && $requiredAnte !== '' && (int) $requiredAnte !== $ante) {
throw new ApiException('免费抽奖注数必须与上一次一致,请修改注数后继续');
}
} }
$lotteryService = LotteryService::getOrCreate($playerId);
$ticketType = LotteryService::drawTicketType($paid, $free);
$configType0 = DiceLotteryPoolConfig::where('name', 'default')->find(); $configType0 = DiceLotteryPoolConfig::where('name', 'default')->find();
$configType1 = DiceLotteryPoolConfig::where('name', 'killScore')->find(); $configType1 = DiceLotteryPoolConfig::where('name', 'killScore')->find();
if (!$configType0) { if (!$configType0) {
throw new ApiException('Lottery pool config not found (name=default required)'); throw new ApiException('Lottery pool config not found (name=default required)');
} }
// 余额校验:统一校验 ante * min(real_ev)
$minEv = DiceRewardConfig::getCachedMinRealEv();
$needMinBalance = abs((float) $minEv) * $ante;
if ($coin < $needMinBalance) {
throw new ApiException('未达抽奖余额 ' . $needMinBalance . ',无法开始游戏');
}
// 付费抽奖:开始前扣除费用 ante * 100不足则提示余额不足
$paidAmount = $ticketType === self::LOTTERY_TYPE_PAID ? ($ante * self::UNIT_COST) : 0;
if ($ticketType === self::LOTTERY_TYPE_PAID && $coin < $paidAmount) {
throw new ApiException('余额不足');
}
// 彩金池累计盈利:用于判断是否触发杀分(不再依赖单个玩家累计盈利) // 彩金池累计盈利:用于判断是否触发杀分(不再依赖单个玩家累计盈利)
// 该值来自 dice_lottery_pool_config.profit_amount // 该值来自 dice_lottery_pool_config.profit_amount
$poolProfitTotal = $configType0->profit_amount ?? 0; $poolProfitTotal = $configType0->profit_amount ?? 0;
@@ -133,7 +162,8 @@ class PlayStartLogic
$rollNumber = (int) ($chosen['grid_number'] ?? 0); $rollNumber = (int) ($chosen['grid_number'] ?? 0);
$realEv = (float) ($chosen['real_ev'] ?? 0); $realEv = (float) ($chosen['real_ev'] ?? 0);
$isTierT5 = (string) ($chosen['tier'] ?? '') === 'T5'; $isTierT5 = (string) ($chosen['tier'] ?? '') === 'T5';
$rewardWinCoin = $isTierT5 ? $realEv : (100 + $realEv); // 玩家始终增加:(100 + real_ev) * ante费用已在开始前扣除免费抽奖同样按该口径结算
$rewardWinCoin = (self::UNIT_COST + $realEv) * $ante;
// 豹子判定5/30 必豹子10/15/20/25 按 DiceRewardConfig 中 BIGWIN 该点数的 weight 判定0-1000010000=100% // 豹子判定5/30 必豹子10/15/20/25 按 DiceRewardConfig 中 BIGWIN 该点数的 weight 判定0-1000010000=100%
// 杀分档位不触发豹子5/30 已在上方抽取时排除10/15/20/25 仅生成非豹子组合 // 杀分档位不触发豹子5/30 已在上方抽取时排除10/15/20/25 仅生成非豹子组合
@@ -166,7 +196,8 @@ class PlayStartLogic
if ($doSuperWin) { if ($doSuperWin) {
$rollArray = $this->getSuperWinRollArray($rollNumber); $rollArray = $this->getSuperWinRollArray($rollNumber);
$isWin = 1; $isWin = 1;
$superWinCoin = $bigWinRealEv > 0 ? $bigWinRealEv : self::SUPER_WIN_BONUS; $bigWinEv = $bigWinRealEv > 0 ? $bigWinRealEv : self::SUPER_WIN_BONUS;
$superWinCoin = (self::UNIT_COST + $bigWinEv) * $ante;
// 中 BIGWIN 豹子:不走原奖励流程,不记录原奖励,不触发 T5 再来一次,仅发放豹子奖金 // 中 BIGWIN 豹子:不走原奖励流程,不记录原奖励,不触发 T5 再来一次,仅发放豹子奖金
$rewardWinCoin = 0; $rewardWinCoin = 0;
$realEv = 0; $realEv = 0;
@@ -203,6 +234,8 @@ class PlayStartLogic
$rewardId, $rewardId,
$configName, $configName,
$ticketType, $ticketType,
$ante,
$paidAmount,
$winCoin, $winCoin,
$superWinCoin, $superWinCoin,
$rewardWinCoin, $rewardWinCoin,
@@ -221,11 +254,13 @@ class PlayStartLogic
'admin_id' => $adminId, 'admin_id' => $adminId,
'lottery_config_id' => $configId, 'lottery_config_id' => $configId,
'lottery_type' => $ticketType, 'lottery_type' => $ticketType,
'ante' => $ante,
'paid_amount' => $paidAmount,
'is_win' => $isWin, 'is_win' => $isWin,
'win_coin' => $winCoin, 'win_coin' => $winCoin,
'super_win_coin' => $superWinCoin, 'super_win_coin' => $superWinCoin,
'reward_win_coin' => $rewardWinCoin, 'reward_win_coin' => $rewardWinCoin,
'use_coins' => 0, 'use_coins' => $paidAmount,
'direction' => $direction, 'direction' => $direction,
'reward_config_id' => $rewardId, 'reward_config_id' => $rewardId,
'start_index' => $startIndex, 'start_index' => $startIndex,
@@ -241,34 +276,40 @@ class PlayStartLogic
throw new \RuntimeException('玩家不存在'); throw new \RuntimeException('玩家不存在');
} }
$coinBefore = (float) $p->coin; $coinBefore = (float) $p->coin;
$coinAfter = $coinBefore + $winCoin; // 开始前先扣付费金额,再加中奖金额(免费抽奖 paid_amount=0
$coinAfter = $coinBefore - $paidAmount + $winCoin;
$p->coin = $coinAfter; $p->coin = $coinAfter;
$p->total_ticket_count = max(0, (int) $p->total_ticket_count - 1); // 不再使用抽奖券作为抽奖条件:付费不扣抽奖次数;免费抽奖仅消耗 free_ticket_count
if ($ticketType === self::LOTTERY_TYPE_PAID) { if ($ticketType === self::LOTTERY_TYPE_FREE) {
$p->paid_ticket_count = max(0, (int) $p->paid_ticket_count - 1);
} else {
$p->free_ticket_count = max(0, (int) $p->free_ticket_count - 1); $p->free_ticket_count = max(0, (int) $p->free_ticket_count - 1);
} }
// 若本局中奖档位为 T5则额外赠送 1 次免费抽奖次数(总次数也 +1并记录抽奖券获取记录 // 若本局中奖档位为 T5则额外赠送 1 次免费抽奖次数(总次数也 +1并记录抽奖券获取记录
if ($isTierT5) { if ($isTierT5) {
$p->free_ticket_count = (int) $p->free_ticket_count + 1; $p->free_ticket_count = (int) $p->free_ticket_count + 1;
$p->total_ticket_count = (int) $p->total_ticket_count + 1;
DicePlayerTicketRecord::create([ DicePlayerTicketRecord::create([
'player_id' => $playerId, 'player_id' => $playerId,
'admin_id' => $adminId, 'admin_id' => $adminId,
'ante' => $ante,
'free_ticket_count' => 1, 'free_ticket_count' => 1,
'remark' => '中奖结果为T5', 'remark' => '中奖结果为T5',
]); ]);
// 记录免费抽奖注数,用于强制下一局注数一致
Cache::set(self::FREE_ANTE_KEY_PREFIX . $playerId, $ante, self::FREE_ANTE_TTL);
} else {
// 若本次消耗了最后一次免费抽奖,则清理注数锁
if ($ticketType === self::LOTTERY_TYPE_FREE && (int) $p->free_ticket_count <= 0) {
Cache::delete(self::FREE_ANTE_KEY_PREFIX . $playerId);
}
} }
$p->save(); $p->save();
// 彩金池累计盈利累加在 name=default 彩金池上: // 彩金池累计盈利累加在 name=default 彩金池上:
// 付费:每局按“当前中奖金额(含 BIGWIN - 抽奖费用 100” // 付费:每局按“当前中奖金额(含 BIGWIN - 抽奖费用ante*100
// 免费券:取消票价成本 100只计入中奖金额 // 免费券:取消票价成本 100只计入中奖金额
$perPlayProfit = ($ticketType === self::LOTTERY_TYPE_PAID) ? ($winCoin - 100.0) : $winCoin; $perPlayProfit = ($ticketType === self::LOTTERY_TYPE_PAID) ? ($winCoin - (float) $paidAmount) : $winCoin;
$addProfit = $perPlayProfit; $addProfit = $perPlayProfit;
try { try {
DiceLotteryPoolConfig::where('id', $type0ConfigId)->update([ DiceLotteryPoolConfig::where('id', $type0ConfigId)->update([
@@ -285,7 +326,8 @@ class PlayStartLogic
DicePlayerWalletRecord::create([ DicePlayerWalletRecord::create([
'player_id' => $playerId, 'player_id' => $playerId,
'admin_id' => $adminId, 'admin_id' => $adminId,
'coin' => $winCoin, // 钱包流水记录本局净变化:-付费金额 + 中奖金额(免费抽奖付费金额为 0
'coin' => $winCoin - (float) $paidAmount,
'type' => self::WALLET_TYPE_DRAW, 'type' => self::WALLET_TYPE_DRAW,
'wallet_before' => $coinBefore, 'wallet_before' => $coinBefore,
'wallet_after' => $coinAfter, 'wallet_after' => $coinAfter,
@@ -336,7 +378,6 @@ class PlayStartLogic
$arr['tier'] = $tier ?? ''; $arr['tier'] = $tier ?? '';
// 记录完数据后返回当前玩家余额与抽奖次数 // 记录完数据后返回当前玩家余额与抽奖次数
$arr['coin'] = $updated ? (float) $updated->coin : 0; $arr['coin'] = $updated ? (float) $updated->coin : 0;
$arr['total_ticket_count'] = $updated ? (int) $updated->total_ticket_count : 0;
return $arr; return $arr;
} }

View File

@@ -64,9 +64,9 @@ class DicePlayRecordController extends BaseController
// 按当前筛选条件统计:平台总盈利 = 付费抽奖(lottery_type=0)次数×100 - 玩家总收益(win_coin 求和) // 按当前筛选条件统计:平台总盈利 = 付费抽奖(lottery_type=0)次数×100 - 玩家总收益(win_coin 求和)
$sumQuery = clone $query; $sumQuery = clone $query;
$playerTotalWin = (float) $sumQuery->sum('win_coin'); $playerTotalWin = (float) $sumQuery->sum('win_coin');
$paidCountQuery = clone $query; $paidAmountQuery = clone $query;
$paidCount = (int) $paidCountQuery->where('lottery_type', 0)->count(); $paidAmount = (float) $paidAmountQuery->where('lottery_type', 0)->sum('paid_amount');
$totalWinCoin = $paidCount * 100 - $playerTotalWin; $totalWinCoin = $paidAmount - $playerTotalWin;
$data = $this->logic->getList($query); $data = $this->logic->getList($query);
$data['total_win_coin'] = $totalWinCoin; $data['total_win_coin'] = $totalWinCoin;

View File

@@ -22,10 +22,13 @@ use think\model\relation\BelongsTo;
* @property $admin_id 关联玩家所属管理员IDDicePlayer.admin_id * @property $admin_id 关联玩家所属管理员IDDicePlayer.admin_id
* @property $lottery_config_id 彩金池配置 * @property $lottery_config_id 彩金池配置
* @property $lottery_type 抽奖类型 * @property $lottery_type 抽奖类型
* @property $ante 底注/注数dice_ante_config.mult
* @property $paid_amount 付费金额(付费局=ante*100免费局=0
* @property $is_win 是否中大奖:豹子号[1,1,1,1,1]~[6,6,6,6,6]为1否则0 * @property $is_win 是否中大奖:豹子号[1,1,1,1,1]~[6,6,6,6,6]为1否则0
* @property $win_coin 赢取平台币(= super_win_coin + reward_win_coin * @property $win_coin 赢取平台币(= super_win_coin + reward_win_coin
* @property $super_win_coin 中大奖平台币(豹子时发放) * @property $super_win_coin 中大奖平台币(豹子时发放)
* @property $reward_win_coin 摇色子中奖平台币 * @property $reward_win_coin 摇色子中奖平台币
* @property $use_coins 消耗平台币(兼容字段:付费局=paid_amount免费局=0
* @property $direction 方向:0=顺时针,1=逆时针 * @property $direction 方向:0=顺时针,1=逆时针
* @property $reward_config_id 奖励配置id * @property $reward_config_id 奖励配置id
* @property $lottery_id 奖池 * @property $lottery_id 奖池

View File

@@ -19,6 +19,7 @@ use think\model\relation\BelongsTo;
* @property $player_id 玩家id * @property $player_id 玩家id
* @property $admin_id 关联玩家所属管理员IDDicePlayer.admin_id * @property $admin_id 关联玩家所属管理员IDDicePlayer.admin_id
* @property $use_coins 消耗硬币 * @property $use_coins 消耗硬币
* @property $ante 底注/注数历史购买记录默认为1T5再来一次写入本次注数
* @property $total_ticket_count 总抽奖次数 * @property $total_ticket_count 总抽奖次数
* @property $paid_ticket_count 购买抽奖次数 * @property $paid_ticket_count 购买抽奖次数
* @property $free_ticket_count 赠送抽奖次数 * @property $free_ticket_count 赠送抽奖次数

View File

@@ -0,0 +1,4 @@
-- DicePlayRecord 新增注数与付费金额字段
ALTER TABLE `dice_play_record`
ADD COLUMN `ante` int unsigned NOT NULL DEFAULT 1 COMMENT '底注/注数(必须为 dice_ante_config.mult 中存在的值)' AFTER `lottery_type`,
ADD COLUMN `paid_amount` int unsigned NOT NULL DEFAULT 0 COMMENT '付费金额(付费局=ante*100免费局=0' AFTER `ante`;

View File

@@ -0,0 +1,3 @@
-- DicePlayerTicketRecord 新增注数字段(用于记录“再来一次”免费抽奖的注数)
ALTER TABLE `dice_player_ticket_record`
ADD COLUMN `ante` int unsigned NOT NULL DEFAULT 1 COMMENT '底注/注数历史购买记录默认为1' AFTER `use_coins`;