新增老虎机游戏:①slot_machine_logs表+模型(8种权重图案/判奖) ②SlotMachineController(扣费/随机/赔付/诅咒/三7全服广播) ③前台面板(三列滚轮动画/逐列停止/赔率说明/历史记录) ④CurrencySource三个枚举

This commit is contained in:
2026-03-01 21:00:21 +08:00
parent dfa7278184
commit 9359184e38
7 changed files with 905 additions and 3 deletions
+12
View File
@@ -81,6 +81,15 @@ enum CurrencySource: string
/** 星海小博士随机事件(好运/坏运/经验/金币奖惩) */
case AUTO_EVENT = 'auto_event';
/** 老虎机转动消耗金币 */
case SLOT_SPIN = 'slot_spin';
/** 老虎机中奖赔付(含本金返还) */
case SLOT_WIN = 'slot_win';
/** 老虎机诅咒额外扣除 */
case SLOT_CURSE = 'slot_curse';
/**
* 返回该来源的中文名称,用于后台统计展示。
*/
@@ -107,6 +116,9 @@ enum CurrencySource: string
self::BACCARAT_BET => '百家乐下注',
self::BACCARAT_WIN => '百家乐赢钱',
self::AUTO_EVENT => '随机事件(星海小博士)',
self::SLOT_SPIN => '老虎机转动',
self::SLOT_WIN => '老虎机中奖',
self::SLOT_CURSE => '老虎机诅咒',
};
}
}
@@ -0,0 +1,249 @@
<?php
/**
* 文件功能:老虎机游戏前台控制器
*
* 提供老虎机转动 API
* - 检查游戏开关、每日限制
* - 扣除金币、生成三列图案
* - 判断结果、赔付金币、写流水
* - 三个7时全服公屏广播
* - 返回结果供前端播放动画
*
* @author ChatRoom Laravel
*
* @version 1.0.0
*/
namespace App\Http\Controllers;
use App\Enums\CurrencySource;
use App\Events\MessageSent;
use App\Jobs\SaveMessageJob;
use App\Models\GameConfig;
use App\Models\SlotMachineLog;
use App\Services\ChatStateService;
use App\Services\UserCurrencyService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class SlotMachineController extends Controller
{
public function __construct(
private readonly UserCurrencyService $currency,
private readonly ChatStateService $chatState,
) {}
/**
* 获取老虎机配置信息(图案表、赔率、今日剩余次数)。
*/
public function info(Request $request): JsonResponse
{
if (! GameConfig::isEnabled('slot_machine')) {
return response()->json(['enabled' => false]);
}
$config = GameConfig::forGame('slot_machine')?->params ?? [];
$user = $request->user();
$dailyLimit = (int) ($config['daily_limit'] ?? 0);
$usedToday = 0;
if ($dailyLimit > 0) {
$usedToday = SlotMachineLog::query()
->where('user_id', $user->id)
->whereDate('created_at', today())
->count();
}
return response()->json([
'enabled' => true,
'cost_per_spin' => (int) ($config['cost_per_spin'] ?? 100),
'daily_limit' => $dailyLimit,
'used_today' => $usedToday,
'remaining' => $dailyLimit > 0 ? max(0, $dailyLimit - $usedToday) : null,
'symbols' => collect(SlotMachineLog::symbols())->map(fn ($s) => $s['emoji']),
]);
}
/**
* 执行一次转动。
*
* 流程:检查可玩 扣费 摇号 赔付 写日志 全服广播(三7)→ 返回结果
*/
public function spin(Request $request): JsonResponse
{
if (! GameConfig::isEnabled('slot_machine')) {
return response()->json(['ok' => false, 'message' => '老虎机未开放。']);
}
$config = GameConfig::forGame('slot_machine')?->params ?? [];
$cost = (int) ($config['cost_per_spin'] ?? 100);
$dailyLimit = (int) ($config['daily_limit'] ?? 0);
$user = $request->user();
// 金币余额检查
if (($user->jjb ?? 0) < $cost) {
return response()->json(['ok' => false, 'message' => "金币不足,每次转动需 {$cost} 金币。"]);
}
// 每日次数限制检查
if ($dailyLimit > 0) {
$usedToday = SlotMachineLog::query()
->where('user_id', $user->id)
->whereDate('created_at', today())
->count();
if ($usedToday >= $dailyLimit) {
return response()->json(['ok' => false, 'message' => "今日已转动 {$dailyLimit} 次,明日再来!"]);
}
}
return DB::transaction(function () use ($user, $cost, $config): JsonResponse {
// ① 扣费
$this->currency->change(
$user,
'gold',
-$cost,
CurrencySource::SLOT_SPIN,
'老虎机转动消耗',
);
// ② 摇号
$r1 = SlotMachineLog::randomSymbol();
$r2 = SlotMachineLog::randomSymbol();
$r3 = SlotMachineLog::randomSymbol();
$resultType = SlotMachineLog::judgeResult($r1, $r2, $r3);
$symbols = SlotMachineLog::symbols();
// ③ 计算赔付金额
$payout = $this->calcPayout($resultType, $cost, $config);
// ④ 赔付金币
if ($payout > 0) {
$this->currency->change(
$user,
'gold',
$payout,
CurrencySource::SLOT_WIN,
"老虎机 {$resultType} 中奖",
);
} elseif ($resultType === 'curse' && ($config['curse_enabled'] ?? true)) {
// 诅咒:再扣一倍本金
$cursePenalty = $cost;
if (($user->jjb ?? 0) >= $cursePenalty) {
$this->currency->change(
$user,
'gold',
-$cursePenalty,
CurrencySource::SLOT_CURSE,
'老虎机三骷髅诅咒额外扣除',
);
$payout = -$cursePenalty; // 净损失 = 本金+惩罚
}
}
// ⑤ 写游戏日志
SlotMachineLog::create([
'user_id' => $user->id,
'reel1' => $r1,
'reel2' => $r2,
'reel3' => $r3,
'result_type' => $resultType,
'cost' => $cost,
'payout' => $payout,
]);
// ⑥ 三个7:全服公屏广播
if ($resultType === 'jackpot') {
$this->broadcastJackpot($user->username, $payout, $cost);
}
$user->refresh();
return response()->json([
'ok' => true,
'reels' => [$r1, $r2, $r3],
'emojis' => [
$symbols[$r1]['emoji'],
$symbols[$r2]['emoji'],
$symbols[$r3]['emoji'],
],
'result_type' => $resultType,
'result_label' => SlotMachineLog::resultLabel($resultType),
'payout' => $payout, // 净变化(正=赢,负=额外亏)
'net_change' => $payout - $cost, // 相对本金的盈亏(debug 用)
'balance' => $user->jjb ?? 0,
]);
});
}
/**
* 查询最近10条个人记录。
*/
public function history(Request $request): JsonResponse
{
$logs = SlotMachineLog::query()
->where('user_id', $request->user()->id)
->orderByDesc('id')
->limit(10)
->get(['id', 'reel1', 'reel2', 'reel3', 'result_type', 'cost', 'payout', 'created_at']);
$symbols = SlotMachineLog::symbols();
return response()->json([
'history' => $logs->map(fn ($l) => [
'emojis' => [$symbols[$l->reel1]['emoji'], $symbols[$l->reel2]['emoji'], $symbols[$l->reel3]['emoji']],
'result_label' => SlotMachineLog::resultLabel($l->result_type),
'payout' => $l->payout,
'created_at' => $l->created_at->format('H:i'),
]),
]);
}
/**
* 计算赔付金额(赢时返还 = 本金 × 赔率)。
*
* @return int 正数=赢得金额(含本金返还),0=不赔付
*/
private function calcPayout(string $resultType, int $cost, array $config): int
{
$multiplier = match ($resultType) {
'jackpot' => (int) ($config['jackpot_payout'] ?? 100),
'triple_gem' => (int) ($config['triple_payout'] ?? 50),
'triple' => (int) ($config['same_payout'] ?? 10),
'pair' => (int) ($config['pair_payout'] ?? 2),
default => 0,
};
return $multiplier > 0 ? $cost * $multiplier : 0;
}
/**
* 三个7全服公屏广播。
*/
private function broadcastJackpot(string $username, int $payout, int $cost): void
{
$net = $payout - $cost;
$content = "🎰🎉【老虎机大奖】恭喜 【{$username}】 转出三个7️⃣!"
.'狂揽 🪙'.number_format($net).' 金币!全服见证奇迹!';
$msg = [
'id' => $this->chatState->nextMessageId(1),
'room_id' => 1,
'from_user' => '系统传音',
'to_user' => '大家',
'content' => $content,
'is_secret' => false,
'font_color' => '#f59e0b',
'action' => '大声宣告',
'sent_at' => now()->toDateTimeString(),
];
$this->chatState->pushMessage(1, $msg);
broadcast(new MessageSent(1, $msg));
SaveMessageJob::dispatch($msg);
}
}
+129
View File
@@ -0,0 +1,129 @@
<?php
/**
* 文件功能:老虎机游戏记录模型
*
* 记录每次转动的三列图案、结果类型和赔付情况。
*
* @author ChatRoom Laravel
*
* @version 1.0.0
*/
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class SlotMachineLog extends Model
{
protected $fillable = [
'user_id', 'reel1', 'reel2', 'reel3',
'result_type', 'cost', 'payout',
];
/**
* 属性类型转换。
*/
protected function casts(): array
{
return [
'cost' => 'integer',
'payout' => 'integer',
];
}
/**
* 关联玩家。
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
/**
* 全部图案符号定义(key => emoji)。
* 顺序影响权重:越靠前出现概率越高(摇奖时按权重随机)。
*
* @return array<string, array{emoji: string, weight: int}>
*/
public static function symbols(): array
{
return [
'cherry' => ['emoji' => '🍒', 'weight' => 30],
'lemon' => ['emoji' => '🍋', 'weight' => 25],
'orange' => ['emoji' => '🍊', 'weight' => 20],
'grape' => ['emoji' => '🍇', 'weight' => 15],
'bell' => ['emoji' => '🔔', 'weight' => 8],
'gem' => ['emoji' => '💎', 'weight' => 4],
'skull' => ['emoji' => '💀', 'weight' => 3],
'seven' => ['emoji' => '7️⃣', 'weight' => 1],
];
}
/**
* 按权重随机抽取一个图案 key。
*/
public static function randomSymbol(): string
{
$symbols = static::symbols();
$totalWeight = array_sum(array_column($symbols, 'weight'));
$rand = random_int(1, $totalWeight);
$cumulative = 0;
foreach ($symbols as $key => $item) {
$cumulative += $item['weight'];
if ($rand <= $cumulative) {
return $key;
}
}
return 'cherry'; // fallback
}
/**
* 根据三列图案判断结果类型。
*
* @return string 'jackpot' | 'triple_gem' | 'triple' | 'pair' | 'curse' | 'miss'
*/
public static function judgeResult(string $r1, string $r2, string $r3): string
{
// 三个7 → 大奖
if ($r1 === 'seven' && $r2 === 'seven' && $r3 === 'seven') {
return 'jackpot';
}
// 三个💎 → 豪华奖
if ($r1 === 'gem' && $r2 === 'gem' && $r3 === 'gem') {
return 'triple_gem';
}
// 三个💀 → 诅咒(亏双倍)
if ($r1 === 'skull' && $r2 === 'skull' && $r3 === 'skull') {
return 'curse';
}
// 三同(其他)
if ($r1 === $r2 && $r2 === $r3) {
return 'triple';
}
// 两同
if ($r1 === $r2 || $r2 === $r3 || $r1 === $r3) {
return 'pair';
}
return 'miss';
}
/**
* 结果类型中文标签。
*/
public static function resultLabel(string $type): string
{
return match ($type) {
'jackpot' => '🎉 三个7大奖!',
'triple_gem' => '💎 三钻豪华奖',
'triple' => '✨ 三同奖',
'pair' => '🎁 两同小奖',
'curse' => '☠️ 三骷髅诅咒',
'miss' => '😔 未中奖',
};
}
}