-- 策划本地流程对照,模拟约定详见 README;不接真实资金。 -- 金额用整数;比例和 RTP 用万分整数,10000 表示 100%。 local function new_demo() local game = { distribution = { blood = 8000, protect = 1000, jp = 500, rake = 500 }, allow_rake_overdraw = false, -- 仅允许抽水击穿,绝不允许奖金击穿 rongduan_val = 0, rongduan_interval_hours = 24, } local room = { min_protect_every_day = 100, max_protect_every_day = 200, check_user_winorlose = true, min_rtp_check = 9600, max_rtp_check = 9800, min_win_percent = 20, max_win_percent = 25, win_period_hours = 24, win_check_interval_seconds = 900, player_win_rtp = 10000, -- 房间周期玩家输赢阈值,与历史 RTP 区间不同 normal_control = "scene", scene_mode = "blood", -- scene: blood/rtp;或 individual protection_enabled = true, max_payout_mode = "amount", max_payout_value = 200, -- ratio 时用万分倍率 fallback_ratio = 10000, } local state = { pools = { blood = 50, protect = 100, jp = 10, rake = 0 }, wallet = 1000, cur_game_return_value = 0, fuse_period_start_at = 0, fuse_last_reset_at = 0, win_period_start_at = 0, win_last_reset_at = 0, last_win_check_at = nil, win_count = 1, lose_count = 4, ratio_satisfied = true, protection_day = 0, cur_rounds = 10, -- 五位示例玩家;本次操作玩家 2,其他玩家用于演示房间人数。 players = { [1] = { bet = 100, payout = 120, class = "win" }, [2] = { bet = 100, payout = 80, class = "lose" }, [3] = { bet = 100, payout = 80, class = "lose" }, [4] = { bet = 100, payout = 80, class = "lose" }, [5] = { bet = 100, payout = 80, class = "lose" }, }, receipts = {}, } local mock = { history_rtp = 9700, candidate_payout = 132, protection_payout = 60, special_payout = 50, protect_precheck_need = 10, -- 演示值;“保护池够”的真实公式待策划 before_commit = nil, -- 测试用:模拟读池后其他玩家已扣款 } local function validate() local sum = 0 for _, name in ipairs({ "blood", "protect", "jp", "rake" }) do local n = game.distribution[name] assert(type(n) == "number" and n >= 0 and n % 1 == 0, "非法分配比例") sum = sum + n end assert(sum == 10000, "四池分配比例合计必须为 100%") assert(game.rongduan_val >= 0 and game.rongduan_interval_hours > 0, "非法熔断配置") assert(type(game.allow_rake_overdraw) == "boolean", "非法抽水击穿开关") for _, bounds in ipairs({ { room.min_protect_every_day, room.max_protect_every_day }, { room.min_rtp_check, room.max_rtp_check }, { room.min_win_percent, room.max_win_percent }, }) do assert(bounds[1] >= 0 and bounds[2] >= bounds[1], "非法配置区间") end assert(room.max_win_percent <= 100, "赢钱人数比例不能超过 100%") assert(room.win_period_hours > 0 and room.win_check_interval_seconds > 0, "非法周期") assert(room.max_payout_mode == "amount" or room.max_payout_mode == "ratio", "非法上限模式") assert(room.max_payout_value >= 0 and room.fallback_ratio > 0, "非法返奖上限") assert(room.normal_control == "scene" or room.normal_control == "individual", "非法控制类型") assert(room.scene_mode == "blood" or room.scene_mode == "rtp", "非法场控模式") end local function tick(now) -- 模拟定时器;spin 也调用一次,避免定时器延迟漏过边界。 -- 不清空奖池余额;这里只切换累计周期。 local fuse_seconds = game.rongduan_interval_hours * 3600 local fuse_start = math.floor(now / fuse_seconds) * fuse_seconds if fuse_start ~= state.fuse_period_start_at then state.cur_game_return_value = 0 state.fuse_period_start_at, state.fuse_last_reset_at = fuse_start, now end local win_seconds = room.win_period_hours * 3600 local win_start = math.floor(now / win_seconds) * win_seconds if win_start ~= state.win_period_start_at then state.players, state.win_count, state.lose_count = {}, 0, 0 state.win_period_start_at, state.win_last_reset_at = win_start, now state.last_win_check_at = nil end local day = math.floor(now / 86400) -- 示例 UTC,实际业务时区待定 if day ~= state.protection_day then state.protection_day, state.cur_rounds = day, 0 end if state.last_win_check_at == nil or now - state.last_win_check_at >= room.win_check_interval_seconds then local n = state.win_count + state.lose_count -- 空房先走常规仅为演示约定;用整数交叉比较避免比例边界误差。 state.ratio_satisfied = n == 0 or ( state.win_count * 100 >= room.min_win_percent * n and state.win_count * 100 <= room.max_win_percent * n) state.last_win_check_at = now end end local function regular_path() if room.normal_control == "individual" then return "individual_rtp" end return "scene_" .. room.scene_mode end local function choose_path(snapshot) local check_protect = false local protection_rounds_enabled = room.min_protect_every_day > 0 or room.max_protect_every_day > 0 if protection_rounds_enabled and state.cur_rounds >= room.min_protect_every_day and state.cur_rounds <= room.max_protect_every_day then check_protect = true elseif room.check_user_winorlose and room.min_rtp_check > 0 and room.max_rtp_check > 0 then if mock.history_rtp < room.min_rtp_check then check_protect = true elseif mock.history_rtp > room.max_rtp_check then return "individual_rtp" elseif room.min_win_percent > 0 and room.max_win_percent > 0 and not state.ratio_satisfied then check_protect = true end end if not check_protect then return regular_path() end -- 关闭保护或保护比例为 0:示例回常规,具体策略含义仍待策划定稿。 if not room.protection_enabled or game.distribution.protect == 0 then return regular_path() end -- 只读预检查;这里不扣、不预占任何奖金。 if snapshot.protect >= mock.protect_precheck_need then return "protection" end return "individual_rtp" end local function contribute(bet) local a = {} for _, name in ipairs({ "blood", "protect", "jp", "rake" }) do a[name] = math.floor(bet * game.distribution[name] / 10000) end -- 示例尾差归血池;正式尾差规则待确认。 a.blood = a.blood + bet - a.blood - a.protect - a.jp - a.rake state.pools.blood = state.pools.blood + a.blood + a.rake state.pools.protect = state.pools.protect + a.protect state.pools.jp = state.pools.jp + a.jp -- 此时 rake 池不增加:抽水份额暂时放在 blood 中。 return a.rake end local function draw_result(path, kind, bet, snapshot) -- 这里替换为实际轴/数学逻辑,接收 path、bet、snapshot。 -- 固定结果只用于演示,id 表示完整合法结果,不能只截断 payout。 -- 保护体验在流程图中单独选轴,不复用常规轴候选。 local normal_payout = path == "protection" and mock.protection_payout or mock.candidate_payout local payout = kind == "zero" and 0 or (kind == "special" and mock.special_payout or normal_payout) assert(payout >= 0 and payout % 1 == 0, "数学结果金额非法") return { id = path .. ":" .. kind, payout = payout, kind = kind } end local function make_debit_plan(path, result) if result.payout == 0 then return {} end -- 已知例子:场控从 blood 扣总返奖。 -- 用户确认保护分支扣保护池;其他奖种/JP/组合规则仍待策划。 local pool = path == "protection" and "protect" or "blood" return { [pool] = result.payout } end local function exceeds_round_cap(bet, payout) if room.max_payout_value == 0 then return false end if room.max_payout_mode == "amount" then return payout > room.max_payout_value end return payout * 10000 > bet * room.max_payout_value end local function commit_result(plan, candidate, zero, wanted_rake) -- 本地模拟的一段不可分割操作;生产须实现同线 Redis 原子操作。 local enough = true for name, amount in pairs(plan) do assert(name ~= "rake" and state.pools[name] ~= nil and amount >= 0, "非法扣池明细") if state.pools[name] < amount then enough = false end end local result = enough and candidate or zero if enough then for name, amount in pairs(plan) do state.pools[name] = state.pools[name] - amount end end -- 必须在确定最终返奖之后算抽水;奖金不足转零时也按此步骤演示。 local actual_rake = wanted_rake if not game.allow_rake_overdraw then actual_rake = math.min(wanted_rake, math.max(0, state.pools.blood)) end state.pools.blood = state.pools.blood - actual_rake state.pools.rake = state.pools.rake + actual_rake return result, actual_rake, enough end local function accumulate(player_id, bet, payout) local p = state.players[player_id] or { bet = 0, payout = 0, class = "neutral" } local old = p.class p.bet, p.payout = p.bet + bet, p.payout + payout local x, y = p.payout * 10000, p.bet * room.player_win_rtp p.class = x > y and "win" or (x < y and "lose" or "neutral") if old ~= p.class then if old == "win" then state.win_count = state.win_count - 1 end if old == "lose" then state.lose_count = state.lose_count - 1 end if p.class == "win" then state.win_count = state.win_count + 1 end if p.class == "lose" then state.lose_count = state.lose_count + 1 end end state.players[player_id] = p -- 人数每局增量更新,ratio_satisfied 等下一个检查时点才刷新。 end local function spin(player_id, round_id, bet, now) local key = tostring(player_id) .. ":" .. tostring(round_id) local receipt = state.receipts[key] if receipt then assert(receipt.bet == bet, "同一局不能修改下注金额") return receipt -- 演示重放;pending 不重新扣注 end validate() assert(type(bet) == "number" and bet > 0 and bet % 1 == 0, "只演示正整数付费下注") tick(now) -- 修正原稿方向;这是入口熔断,不是最后一笔的硬返奖上限。 if game.rongduan_val > 0 and state.cur_game_return_value >= game.rongduan_val then return { status = "rejected", reason = "熔断" } end if state.wallet < bet then return { status = "rejected", reason = "余额不足" } end receipt = { status = "pending", bet = bet } state.receipts[key] = receipt state.wallet = state.wallet - bet local wanted_rake = contribute(bet) local snapshot = { blood = state.pools.blood, protect = state.pools.protect, jp = state.pools.jp } local path = choose_path(snapshot) local candidate = draw_result(path, "normal", bet, snapshot) if exceeds_round_cap(bet, candidate.payout) then candidate = draw_result(path, "special", bet, snapshot) if exceeds_round_cap(bet, candidate.payout) or candidate.payout * 10000 >= bet * room.fallback_ratio then receipt.reason = "特殊轴未给出满足上限及严格低倍数条件的结果" return receipt -- 异常待处理,不无限重抽、不自动当成池不足 end end local zero = draw_result(path, "zero", bet, snapshot) local plan = make_debit_plan(path, candidate) receipt.candidate, receipt.zero, receipt.path = candidate, zero, path -- 生产必须先持久确认候选和零奖原字节,再实际扣池。 if mock.before_commit then mock.before_commit(state) end local result, actual_rake, enough = commit_result(plan, candidate, zero, wanted_rake) state.wallet = state.wallet + result.payout state.cur_game_return_value = state.cur_game_return_value + result.payout state.cur_rounds = state.cur_rounds + 1 accumulate(player_id, bet, result.payout) receipt.status, receipt.result = "final", result receipt.actual_rake, receipt.pool_enough = actual_rake, enough return receipt end return { game = game, room = room, state = state, mock = mock, spin = spin, tick = tick, choose_path = choose_path } end -- 示例调用:血池 50,保护 100,JP 10;下注 100,场控返奖 132。 local demo = new_demo() local result = demo.spin(2, "round-1", 100, 1) print(result.path, result.result.payout, demo.state.pools.blood, demo.state.pools.rake) -- 关闭击穿:scene_blood 132 0 3 -- 若在 spin 前设置 demo.game.allow_rake_overdraw = true,则为 132 -2 5。 return { new_demo = new_demo }