steps;
-
- private BigDecimal totalWin;
-
- private Long recordId;
- private Long boxRecordId;
-
- /** 身份维度 common/anchor/robot/white,白号隔离依据 */
- private String audience;
- /** 是否白号:白号不注水/不扣水/不受水位约束 */
- private boolean blankAccount;
-
- @Data
- public static class CachedStep implements Serializable {
- private int freeSpinId;
- private int cascadeSeq;
- private String gridState;
- private String multiplierDetail;
- private String symbolLinks; // 本旋累积连线(含 tumble)
- private BigDecimal winAmount; // 本旋累计(该旋末帧 = 这一旋的总额)
- private BigDecimal spinWin; // 本局累计(末帧 = totalWin)
- /** 本步后炸弹累计倍数(FS 跨旋传递);断线重连时前端靠它恢复累计倍数 */
- private BigDecimal accumulatorAfter;
- private int winCapped;
- private String bigwinLevel;
- private int isFinal;
- private String retriggerInfo;
- private String dropData;
- private String fillData;
- private String scatterTrigger;
- }
-}
diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/model/cache/OlympusRewardPoolCache.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/model/cache/OlympusRewardPoolCache.java
deleted file mode 100644
index ed791b9..0000000
--- a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/model/cache/OlympusRewardPoolCache.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package com.ruoyi.game.olympus.model.cache;
-
-import lombok.Data;
-
-import java.math.BigDecimal;
-
-/**
- * Olympus 按箱奖池(水位)缓存。
- * 水位 = totalBet - totalWin + adjust(人工注水/抽水),仅真实玩家(白号隔离)。
- */
-@Data
-public class OlympusRewardPoolCache {
- private BigDecimal totalBet;
- private BigDecimal totalWin;
- private BigDecimal adjust;
-}
diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/repository/OlympusCacheRepository.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/repository/OlympusCacheRepository.java
deleted file mode 100644
index c8b151c..0000000
--- a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/repository/OlympusCacheRepository.java
+++ /dev/null
@@ -1,30 +0,0 @@
-package com.ruoyi.game.olympus.repository;
-
-import com.ruoyi.common.core.redis.RedisCache;
-import com.ruoyi.game.olympus.model.cache.OlympusGameCache;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Component;
-
-import java.util.concurrent.TimeUnit;
-
-import static com.ruoyi.admin.config.RedisConstants.OLYMPUS_GAME_CACHE;
-
-/** 单局进行中缓存(Redis,TTL 30 分钟) */
-@Component
-public class OlympusCacheRepository {
-
- @Autowired
- private RedisCache redisCache;
-
- public OlympusGameCache getCache(Long userId) {
- return redisCache.getCacheObject(OLYMPUS_GAME_CACHE + userId);
- }
-
- public void setCache(Long userId, OlympusGameCache cache) {
- redisCache.setCacheObject(OLYMPUS_GAME_CACHE + userId, cache, 30, TimeUnit.MINUTES);
- }
-
- public void clearCache(Long userId) {
- redisCache.deleteObject(OLYMPUS_GAME_CACHE + userId);
- }
-}
diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/repository/OlympusCycleRepository.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/repository/OlympusCycleRepository.java
deleted file mode 100644
index bf96b9a..0000000
--- a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/repository/OlympusCycleRepository.java
+++ /dev/null
@@ -1,81 +0,0 @@
-package com.ruoyi.game.olympus.repository;
-
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.data.redis.core.RedisTemplate;
-import org.springframework.stereotype.Component;
-
-import java.math.BigDecimal;
-import java.math.RoundingMode;
-import java.util.concurrent.TimeUnit;
-
-import static com.ruoyi.admin.config.RedisConstants.OLYMPUS_GAME_CYCLE;
-
-/**
- * 一轮内 RTP 动态收敛账本,按 (boxId, audience) 隔离。
- * Hash key = ruoyi:olympus:cycle:{boxId}:{audience},字段 drawn/mulSum/conv:{ornamentId}。
- * 白号(audience=white)也有独立账本,与其他身份完全隔离。
- */
-@Component
-public class OlympusCycleRepository {
-
- @Autowired
- public RedisTemplate redisTemplate;
-
- private static final String F_DRAWN = "drawn";
- private static final String F_MUL_SUM = "mulSum";
- private static final String F_CONV_PREFIX = "conv:";
- private static final long TTL_DAYS = 30;
-
- private String key(Long boxId, String audience) {
- return OLYMPUS_GAME_CYCLE + boxId + ":" + audience;
- }
-
- public long drawn(Long boxId, String audience) {
- Object v = redisTemplate.opsForHash().get(key(boxId, audience), F_DRAWN);
- return v == null ? 0L : toLong(v);
- }
-
- public double mulSum(Long boxId, String audience) {
- Object v = redisTemplate.opsForHash().get(key(boxId, audience), F_MUL_SUM);
- return v == null ? 0d : toDouble(v);
- }
-
- /** 记一抽:drawn+1、mulSum+actualMul;收敛阶段额外 conv:{ornamentId}+1。 */
- public void recordDraw(Long boxId, String audience, Integer ornamentId, BigDecimal actualMul, boolean converge) {
- String k = key(boxId, audience);
- redisTemplate.opsForHash().increment(k, F_DRAWN, 1L);
- double mul = actualMul != null ? actualMul.doubleValue() : 0d;
- if (mul != 0d) redisTemplate.opsForHash().increment(k, F_MUL_SUM, mul);
- if (converge && ornamentId != null) {
- redisTemplate.opsForHash().increment(k, F_CONV_PREFIX + ornamentId, 1L);
- }
- redisTemplate.expire(k, TTL_DAYS, TimeUnit.DAYS);
- }
-
- public void resetCycle(Long boxId, String audience) {
- redisTemplate.delete(key(boxId, audience));
- }
-
- public long convergeCount(Long boxId, String audience, Integer ornamentId) {
- Object v = redisTemplate.opsForHash().get(key(boxId, audience), F_CONV_PREFIX + ornamentId);
- return v == null ? 0L : toLong(v);
- }
-
- /** 本轮实时 RTP(%) = mulSum / drawn / cm × 100(common 身份口径,供后台展示)。 */
- public BigDecimal realtimeRtp(Long boxId, String audience, double cm) {
- long d = drawn(boxId, audience);
- if (d <= 0 || cm <= 0) return null;
- double rtp = mulSum(boxId, audience) / d / cm * 100;
- return BigDecimal.valueOf(rtp).setScale(2, RoundingMode.HALF_UP);
- }
-
- private static long toLong(Object v) {
- if (v instanceof Number) return ((Number) v).longValue();
- try { return Long.parseLong(v.toString().trim()); } catch (Exception e) { return 0L; }
- }
-
- private static double toDouble(Object v) {
- if (v instanceof Number) return ((Number) v).doubleValue();
- try { return Double.parseDouble(v.toString().trim()); } catch (Exception e) { return 0d; }
- }
-}
diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/repository/OlympusRewardPoolCacheRepository.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/repository/OlympusRewardPoolCacheRepository.java
deleted file mode 100644
index 6aa960c..0000000
--- a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/repository/OlympusRewardPoolCacheRepository.java
+++ /dev/null
@@ -1,115 +0,0 @@
-package com.ruoyi.game.olympus.repository;
-
-import com.ruoyi.game.olympus.model.cache.OlympusRewardPoolCache;
-import com.ruoyi.game.olympus.mapper.OlympusRecordMapper;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.data.redis.core.RedisTemplate;
-import org.springframework.stereotype.Component;
-
-import java.math.BigDecimal;
-import java.util.Map;
-import java.util.concurrent.TimeUnit;
-
-import static com.ruoyi.admin.config.RedisConstants.OLYMPUS_GAME_REWARD_POOL_CACHE;
-
-/**
- * Olympus 按【箱子 × 身份】奖池(水位)仓储 —— 对齐荒野争锋 bullet 的按箱水位模型,再按身份(audience)拆池。
- * 每箱每身份独立键::box:{boxId}:{audience}:dbbet / :dbwin / :adjust;全局 adjust 也按身份一份。
- * 水位 = box:bet − box:win + box:adjust(可能为负)。
- *
- * 记账口径(方案B''):bet/win **由 DB 已结算记录按身份派生**(game_olympus_record + tt_user),
- * Redis 仅做短缓存供 payoutCap/展示快速读取;adjust 为管理员人工注水/抽水(Redis)。
- * 因 record 与发奖同一事务提交,池值恒等于已提交的真实账,无"提交后记账崩溃窗口"。
- */
-@Component
-public class OlympusRewardPoolCacheRepository {
-
- @Autowired
- public RedisTemplate redisTemplate;
-
- @Autowired
- private OlympusRecordMapper recordMapper;
-
- private static final String KEY_BOX_PREFIX = OLYMPUS_GAME_REWARD_POOL_CACHE + ":box:";
- /** 全局人工调整累计(各箱 adjust 之和,仅展示),按身份一份 */
- private static final String KEY_ADJUST = OLYMPUS_GAME_REWARD_POOL_CACHE + ":adjust:";
-
- private static final String KEY_DB_BET = ":dbbet";
- private static final String KEY_DB_WIN = ":dbwin";
- /** ponytail: DB 派生结果短缓存;DB 为真相,TTL 内只是短暂滞后、自愈,无永久漂移 */
- private static final long DB_CACHE_TTL_SECONDS = 5;
-
- private static String boxKey(Long boxId, String audience, String suffix) {
- return KEY_BOX_PREFIX + boxId + ":" + audience + suffix;
- }
-
- private static String adjustKey(String audience) {
- return KEY_ADJUST + audience;
- }
-
- /** 管理员逐箱注水/抽水:delta 为正注水、为负抽水;同时累加该身份的全局 adjust 键 */
- public void adjustBox(Long boxId, String audience, BigDecimal delta) {
- if (boxId == null || delta == null || delta.signum() == 0) return;
- redisTemplate.opsForValue().increment(boxKey(boxId, audience, ":adjust"), delta.doubleValue());
- redisTemplate.opsForValue().increment(adjustKey(audience), delta.doubleValue());
- }
-
- /** 仅调整全局 adjust 键(重置已删除箱子遗留残余),不动箱子键 */
- public void adjustGlobal(String audience, BigDecimal delta) {
- if (delta == null || delta.signum() == 0) return;
- redisTemplate.opsForValue().increment(adjustKey(audience), delta.doubleValue());
- }
-
- public BigDecimal getGlobalAdjust(String audience) {
- return readValue(adjustKey(audience));
- }
-
- public OlympusRewardPoolCache getBoxCache(Long boxId, String audience) {
- OlympusRewardPoolCache cache = new OlympusRewardPoolCache();
- cache.setTotalBet(dbPoolTotal(boxId, audience, KEY_DB_BET));
- cache.setTotalWin(dbPoolTotal(boxId, audience, KEY_DB_WIN));
- cache.setAdjust(readValue(boxKey(boxId, audience, ":adjust")));
- return cache;
- }
-
- public BigDecimal getBoxAdjust(Long boxId, String audience) {
- return readValue(boxKey(boxId, audience, ":adjust"));
- }
-
- /** 箱子×身份水位 = 该身份箱内累计投注 − 累计返奖 + 人工调整(可能为负) */
- public BigDecimal getBoxBalance(Long boxId, String audience) {
- OlympusRewardPoolCache c = getBoxCache(boxId, audience);
- return c.getTotalBet().subtract(c.getTotalWin()).add(c.getAdjust())
- .setScale(2, java.math.RoundingMode.HALF_UP);
- }
-
- /** 读取某箱某身份 bet/win 的 DB 派生值(短缓存;缓存缺失时按 record 聚合重算) */
- private BigDecimal dbPoolTotal(Long boxId, String audience, String suffix) {
- String key = boxKey(boxId, audience, suffix);
- Object v = redisTemplate.opsForValue().get(key);
- if (v != null) return toBd(v);
- refreshDbPool(boxId, audience);
- return toBd(redisTemplate.opsForValue().get(key));
- }
-
- private void refreshDbPool(Long boxId, String audience) {
- Map row = boxId != null ? recordMapper.sumPool(boxId, audience) : null;
- BigDecimal bet = toBd(row != null ? row.get("totalBet") : null);
- BigDecimal win = toBd(row != null ? row.get("totalWin") : null);
- redisTemplate.opsForValue().set(boxKey(boxId, audience, KEY_DB_BET), bet.doubleValue(), DB_CACHE_TTL_SECONDS, TimeUnit.SECONDS);
- redisTemplate.opsForValue().set(boxKey(boxId, audience, KEY_DB_WIN), win.doubleValue(), DB_CACHE_TTL_SECONDS, TimeUnit.SECONDS);
- }
-
- private BigDecimal readValue(String key) {
- return toBd(redisTemplate.opsForValue().get(key));
- }
-
- private static BigDecimal toBd(Object v) {
- if (v == null) return BigDecimal.ZERO;
- try {
- return new BigDecimal(v.toString());
- } catch (Exception e) {
- return BigDecimal.ZERO;
- }
- }
-}
diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/service/OlympusDistributionService.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/service/OlympusDistributionService.java
deleted file mode 100644
index 464ac0b..0000000
--- a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/service/OlympusDistributionService.java
+++ /dev/null
@@ -1,354 +0,0 @@
-package com.ruoyi.game.olympus.service;
-
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.fasterxml.jackson.core.type.TypeReference;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.ruoyi.game.olympus.domain.OlympusBox;
-import com.ruoyi.game.olympus.domain.OlympusBoxOrnament;
-import com.ruoyi.game.olympus.domain.OlympusBoxTemplate;
-import com.ruoyi.game.olympus.domain.OlympusOrnament;
-import com.ruoyi.game.olympus.enums.OlympusModeType;
-import com.ruoyi.game.olympus.mapper.OlympusBoxMapper;
-import com.ruoyi.game.olympus.mapper.OlympusBoxOrnamentMapper;
-import com.ruoyi.game.olympus.mapper.OlympusBoxTemplateMapper;
-import com.ruoyi.game.olympus.mapper.OlympusOrnamentMapper;
-import com.ruoyi.game.olympus.mapper.OlympusRoundMapper;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
-
-import java.util.*;
-
-/**
- * Olympus 饰品桶分布计算 / 重置服务(按身份 audience)。
- * 算法移植自 sweet:目标 RTP → 幂律权重二分 → 单调约束 → 微调逼近;
- * 差异:expect_mul 从 game_olympus_ornament 读,基础权重按模式桶区间通用生成(30×0.6^k)。
- */
-@Service
-public class OlympusDistributionService {
-
- private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
-
- @Autowired private OlympusBoxMapper boxMapper;
- @Autowired private OlympusBoxOrnamentMapper boxOrnamentMapper;
- @Autowired private OlympusBoxTemplateMapper boxTemplateMapper;
- @Autowired private OlympusOrnamentMapper ornamentMapper;
- @Autowired private OlympusRoundMapper roundMapper;
-
- /**
- * 桶"实际发出期望"覆盖:桶 17 展示 1000x(区间 630~1370),但单局封顶 1000,
- * 真正发出去的是 min(原始分, 1000)。理论值(原始分均匀分布)E[min(x,1000)] = 907.5;
- * 实测(buy_fs 10 箱 × 桶17 全部回合 = 200 个)均值 834.6、中位 821.3、27.5% 触顶,
- * 因为生成端种的大奖目标集中在 1000 附近而不是均匀分布。
- * 配比计算(calcRounds)与后台「目标RTP」都按"实际会发出去多少钱"算,所以用实测值:
- * 用名义 1000 会把 RTP 高估约 0.5~1.6 个百分点。
- * 注:抽回合时的 payoutCap 过滤(OlympusRoundPicker)仍读 DB 的名义 expect_mul,偏保守、保留安全余量。
- */
- private static final Map PAYOUT_EXPECT_OVERRIDE = Map.of(17, 835.0);
-
- /** 饰品桶 ID → 实际发出期望(受封顶影响的桶用覆盖值,其余用 DB 的 expect_mul)。 */
- public Map loadOrnamentMul() {
- Map m = new HashMap<>();
- for (OlympusOrnament o : ornamentMapper.selectList(null)) {
- if (o.getId() == null) continue;
- int id = o.getId().intValue();
- Double override = PAYOUT_EXPECT_OVERRIDE.get(id);
- if (override != null) { m.put(id, override); continue; }
- m.put(id, o.getExpectMul() != null ? o.getExpectMul().doubleValue() : 0.0);
- }
- return m;
- }
-
- /** 该桶池子里一条回合都没有时才回退名义期望:只要池子有回合,它的均值就是这个箱子的真实期望 */
- private static final int MIN_POOL_SAMPLE = 1;
-
- /**
- * 桶 → **这个箱子的池子实际会赔出去多少钱**的期望(AVG(actual_mul),按 boxId 统计)。
- *
- * 为什么不用名义 expect_mul:桶内实际均值与区间中点有偏差(实测 -18% ~ +11%,桶 17 还受 1000x 封顶
- * 影响只有 ~835),用名义值算配比 → 「设 80% 实际赔 81.8%」这种系统性偏差;而且每个箱子的池子是
- * 独立生成的一批回合,只有按它自己的池内均值算,算出来的配比才真的等于该箱的期望赔付。
- * 样本不足的桶回退名义值(池子还没生成时也能算)。
- */
- public Map loadPoolExpect(Long boxId) {
- Map expect = new HashMap<>(loadOrnamentMul());
- if (boxId == null) return expect;
- try {
- for (Map row : roundMapper.avgMulByOrnament(boxId)) {
- Object idObj = row.get("ornamentId");
- Object nObj = row.get("sampleCount");
- Object avgObj = row.get("avgMul");
- if (idObj == null || avgObj == null) continue;
- int id = ((Number) idObj).intValue();
- long n = nObj instanceof Number ? ((Number) nObj).longValue() : 0L;
- double avg = ((Number) avgObj).doubleValue();
- if (!expect.containsKey(id)) continue;
- if (n < MIN_POOL_SAMPLE || avg <= 0) continue;
- expect.put(id, avg);
- }
- } catch (Exception ignore) {
- // 池子还没生成/查询异常 → 用名义值兜底
- }
- return expect;
- }
-
- /**
- * 按目标 RTP 动态计算各饰品桶回合数(幂律 power 二分搜索 + 单调 + 微调)。
- * 期望值用 {@link #loadPoolExpect}(**该箱**池内实际均值),所以算出来的配比实际赔付就等于 targetRtp。
- * @param mode 模式 @param boxId 箱子(决定用哪个池子的实际均值) @param totalRounds 固定池回合数 @param targetRtp 目标 RTP(如 0.8)
- */
- public Map calcRounds(OlympusModeType mode, Long boxId, int totalRounds, double targetRtp) {
- double cm = mode.getCostMultiplier();
- double targetAvg = targetRtp * cm;
- Map ornMul = loadPoolExpect(boxId);
-
- // 基础权重:模式桶区间 [lo,hi] 内 30×0.6^k 等比递减
- int[] range = mode.getOrnamentRange();
- int lo = range[0], hi = range[1];
- int n = Math.max(1, hi - lo + 1);
- double[][] baseWeights = new double[n][2];
- for (int i = 0; i < n; i++) {
- baseWeights[i][0] = lo + i;
- baseWeights[i][1] = 30.0 * Math.pow(0.6, i);
- }
-
- int nOrns = baseWeights.length;
- // 回合数不足:每桶至少 1,均摊
- if (totalRounds <= nOrns) {
- Map flat = new LinkedHashMap<>();
- for (double[] w : baseWeights) flat.put((int) w[0], 1);
- return flat;
- }
- int remainingRounds = totalRounds - nOrns;
-
- double guaranteedMulSum = 0;
- for (double[] w : baseWeights) guaranteedMulSum += ornMul.getOrDefault((int) w[0], 0.0);
- double targetTotalMul = targetAvg * totalRounds;
- double remainingTargetMul = targetTotalMul - guaranteedMulSum;
- double remainingTargetAvg = remainingTargetMul / remainingRounds;
-
- // 二分 power
- double power = 1.0, plo = 0.2, phi = 10.0;
- for (int iter = 0; iter < 30; iter++) {
- double sumW = 0, sumWMul = 0;
- for (double[] w : baseWeights) {
- double weight = Math.pow(w[1], power);
- sumW += weight;
- sumWMul += weight * ornMul.getOrDefault((int) w[0], 0.0);
- }
- double avg = sumW > 0 ? sumWMul / sumW : 0;
- if (Math.abs(avg - remainingTargetAvg) < Math.max(remainingTargetAvg * 0.01, 0.001)) break;
- if (avg < remainingTargetAvg) phi = power; else plo = power;
- power = (plo + phi) / 2;
- }
-
- Map result = new LinkedHashMap<>();
- for (double[] w : baseWeights) result.put((int) w[0], 1);
- double totalW = 0;
- Map adjWeights = new LinkedHashMap<>();
- for (double[] w : baseWeights) {
- double weight = Math.pow(w[1], power);
- adjWeights.put((int) w[0], weight);
- totalW += weight;
- }
- int rem = remainingRounds;
- for (double[] w : baseWeights) {
- int ornId = (int) w[0];
- int extra = totalW > 0 ? (int) Math.round(adjWeights.get(ornId) / totalW * remainingRounds) : 0;
- int put = Math.min(extra, rem);
- if (put > 0) { result.put(ornId, result.get(ornId) + put); rem -= put; }
- }
- if (rem > 0) { int first = (int) baseWeights[0][0]; result.put(first, result.get(first) + rem); }
-
- // 单调:count[i] >= count[i+1]
- List sortedIds = new ArrayList<>(result.keySet());
- sortedIds.sort(Integer::compareTo);
- for (int i = sortedIds.size() - 2; i >= 0; i--) {
- int cur = sortedIds.get(i), next = sortedIds.get(i + 1);
- if (result.get(cur) < result.get(next)) result.put(cur, result.get(next));
- }
- int curTotal = result.values().stream().mapToInt(Integer::intValue).sum();
- int diff = totalRounds - curTotal;
- if (diff != 0) { int first = sortedIds.get(0); result.put(first, result.get(first) + diff); }
-
- // 微调:任取两个桶搬 1 个(保持单调),只要更接近目标
- // (原来只在"最大的 3 个桶"之间搬,池内均值口径下根本走不到目标,会差 0.5~1 个百分点)
- for (int iter = 0; iter < 400; iter++) {
- double currentMul = 0;
- for (Map.Entry e : result.entrySet())
- currentMul += e.getValue() * ornMul.getOrDefault(e.getKey(), 0.0);
- double deltaMul = targetAvg * totalRounds - currentMul;
- if (Math.abs(deltaMul) < 0.01) break;
- int bestLo = -1, bestHi = -1;
- double bestDiff = Math.abs(deltaMul);
- for (int loId : sortedIds) {
- if (result.get(loId) <= 1) continue;
- for (int hiId : sortedIds) {
- if (loId == hiId) continue;
- double change = ornMul.getOrDefault(hiId, 0.0) - ornMul.getOrDefault(loId, 0.0);
- if (Math.abs(change) < 0.001) continue;
- double nd = Math.abs(deltaMul - change);
- if (nd < bestDiff) {
- boolean ok = true;
- int ca = result.get(loId) - 1, idxLo = sortedIds.indexOf(loId);
- if (idxLo < sortedIds.size() - 1 && ca < result.get(sortedIds.get(idxLo + 1))) ok = false;
- int ca2 = result.get(hiId) + 1, idxHi = sortedIds.indexOf(hiId);
- if (idxHi > 0 && ca2 > result.get(sortedIds.get(idxHi - 1))) ok = false;
- if (sortedIds.indexOf(loId) < sortedIds.indexOf(hiId) && ca < ca2) ok = false;
- if (ok) { bestDiff = nd; bestLo = loId; bestHi = hiId; }
- }
- }
- }
- if (bestLo < 0) break;
- result.put(bestLo, result.get(bestLo) - 1);
- result.put(bestHi, result.get(bestHi) + 1);
- }
- return result;
- }
-
- /** 模拟 payoutCap 约束下的实际 RTP(%):桶的赔付期望按该箱池内实际均值算。 */
- public double simulateActualRtp(Map distribution, Long boxId, double rtp, int simRounds) {
- if (distribution == null || distribution.isEmpty() || simRounds <= 0) return 0;
- Map ornMul = loadPoolExpect(boxId);
- double cm = 1.0;
- if (boxId != null) {
- OlympusBox box = boxMapper.selectById(boxId);
- if (box != null) cm = OlympusModeType.fromBoxType(box.getBoxType()).getCostMultiplier();
- }
- if (cm <= 0) return 0;
- List mulList = new ArrayList<>();
- for (Map.Entry e : distribution.entrySet()) {
- if (e.getValue() == null || e.getValue() <= 0) continue;
- double mul = ornMul.getOrDefault(e.getKey(), 0.0);
- for (int i = 0; i < e.getValue(); i++) mulList.add(mul);
- }
- if (mulList.isEmpty()) return 0;
- Random rnd = new Random();
- double totalRtpSum = 0;
- for (int round = 0; round < simRounds; round++) {
- Collections.shuffle(mulList, rnd);
- double balance = 0, totalPayout = 0, totalCost = 0;
- for (double mul : mulList) {
- double payoutCap = Math.max(0, balance + cm) * rtp;
- double actualMul = (mul <= payoutCap) ? mul : 0;
- balance += cm - actualMul;
- totalPayout += actualMul;
- totalCost += cm;
- }
- totalRtpSum += totalCost > 0 ? totalPayout / totalCost : 0;
- }
- return (totalRtpSum / simRounds) * 100;
- }
-
- /** 由固定池回合数反推总回合 N = round(fixed/(1-cr))。 */
- public static int deriveTotalRounds(int fixedRounds, double convergeRatio) {
- double cr = convergeRatio < 0 ? 0 : (convergeRatio > 0.9 ? 0.9 : convergeRatio);
- return (int) Math.round(fixedRounds / (1.0 - cr));
- }
-
- /** 由总回合 N 算固定池回合数 fixed = round(N*(1-cr))。 */
- public static int deriveFixedRounds(int totalRounds, double convergeRatio) {
- double cr = convergeRatio < 0 ? 0 : (convergeRatio > 0.9 ? 0.9 : convergeRatio);
- return (int) Math.round(totalRounds * (1.0 - cr));
- }
-
- private static double convergeRatioOf(OlympusBox box) {
- double cr = (box != null && box.getRtpConvergeRatio() != null) ? box.getRtpConvergeRatio().doubleValue() : 0.15;
- if (cr < 0) cr = 0;
- if (cr > 0.9) cr = 0.9;
- return cr;
- }
-
- /** 重算某箱某身份分布:按目标 RTP 分配固定池 count,used 归 0。 */
- @Transactional(rollbackFor = Exception.class)
- public void reset(Long boxId, String audience) {
- OlympusBox box = boxMapper.selectById(boxId);
- if (box == null) return;
- OlympusModeType mode = OlympusModeType.fromBoxType(box.getBoxType());
- int totalRounds = box.getTotalOrnaments() != null ? box.getTotalOrnaments() : 200;
- double targetRtp = box.getRtp() != null ? box.getRtp().doubleValue() : 0.8;
- int fixedRounds = deriveFixedRounds(totalRounds, convergeRatioOf(box));
- if (fixedRounds < 1) fixedRounds = totalRounds;
- Map dist = calcRounds(mode, boxId, fixedRounds, targetRtp);
- List list = new ArrayList<>();
- for (Map.Entry e : dist.entrySet()) {
- if (e.getValue() == null || e.getValue() <= 0) continue;
- OlympusBoxOrnament bo = new OlympusBoxOrnament();
- bo.setBoxId(boxId);
- bo.setOrnamentId(e.getKey());
- bo.setAudience(audience);
- bo.setCount(e.getValue());
- bo.setUsed(0);
- list.add(bo);
- }
- boxOrnamentMapper.deleteByBoxIdAndAudience(boxId, audience);
- if (!list.isEmpty()) boxOrnamentMapper.batchInsert(list);
- }
-
- /** 应用模板分布到某箱某身份(count 取模板、used 归 0),并按固定池反推回写总回合。 */
- @Transactional(rollbackFor = Exception.class)
- public void writeDistribution(Long boxId, String audience, List