JUEGOFortune Tiger

Metodología de las pruebas del agente

Por Jack · Actualizado el

Las «pruebas del agente» de este sitio son simulaciones por computadora, no partidas en el juego real. Usan un modelo de Fortune Tiger construido con tres datos publicados (RTP, frecuencia de premio y tabla de pagos) y un supuesto nuestro: con qué probabilidad sale cada premio, porque el proveedor no lo publica. Aquí está todo lo necesario para revisarlas o repetirlas, incluido el código.

1. Datos de entrada (con fuente)

RTP96.81 %[1][2]
Frecuencia de premio23.15 % de los giros[1] (22.16 % en el juego base + 0.99 % de la función Fortune Tiger)
Premio máximo2,500x la apuesta[1][2]
Tabla de pagos50x, 20x, 5x, 2x, 1.6x, 1x, 0.6x y su versión x10 (pantalla llena)[2][1]

2. Lo que suponemos (no está publicado)

Probabilidad supuesta de cada resultado
  • 0.60x — 1 de cada 13 giros
  • 1.00x — 1 de cada 20 giros
  • 1.60x — 1 de cada 30 giros
  • 2.00x — 1 de cada 36 giros
  • 5.00x — 1 de cada 75 giros
  • 6.00x — 1 de cada 87 giros
  • 10.00x — 1 de cada 133 giros
  • 16.00x — 1 de cada 195 giros
  • 20.00x — 1 de cada 234 giros
  • 50.00x — 1 de cada 496 giros
  • 200.00x — 1 de cada 1,545 giros
  • 500.00x — 1 de cada 3,273 giros
  • 2,500.00x — 1 de cada 1,000,000 giros

Retorno esperado del modelo: 96.8100 %.

3. Experimentos y tamaño de muestra

ExperimentoMuestraSemilla
Calculadora: sesiones de 100 y 1,000 giros10,000 sesiones por tamaño, apuesta de $120260927
Horarios: 24 horas con las mismas probabilidades24 × 20,000 giros = 480,000 giros, apuesta de $1; repetido con una segunda semilla20260929 y 7
Rachas: premio después de giros sin premio2,000,000 giros seguidos20260929
Martingala contra apuesta fija10,000 sesiones de hasta 500 giros, presupuesto de $200, apuesta inicial de $1, tope de $25020260929

Usamos un generador pseudoaleatorio con semilla fija (mulberry32), así que cualquiera que corra el código con la misma semilla obtiene exactamente las mismas tablas. Las simulaciones se calculan al construir el sitio, no en tu navegador (salvo el botón «Simula tu propia sesión» de la calculadora, que usa una semilla aleatoria).

4. Límites

5. Código del modelo

Este es el código completo que genera todas las pruebas (JavaScript, sin dependencias). Puedes copiarlo y correrlo con Node.js.

Ver el código (sim.mjs)
// Illustrative slot model for the "1,000 giros" simulator.
// Only two inputs come from sources: RTP and hit frequency. The shape of the prize
// distribution is OUR assumption (stated on the page); it is not the real game's paytable.

// Relative prize tiers (x bet) and their share of all winning spins. Values are scaled
// so the model's expected return equals the RTP exactly.
const TIERS = [
  { x: 0.4, w: 0.30 },
  { x: 1, w: 0.25 },
  { x: 2, w: 0.18 },
  { x: 4, w: 0.12 },
  { x: 8, w: 0.09 },
  { x: 15, w: 0.045 },
  { x: 40, w: 0.012 },
  { x: 150, w: 0.0025 },
  { x: 500, w: 0.0003 },
];
const MAX_WIN_P = 1e-6; // probability per spin of the max win (x maxWin), fixed, not scaled

export function buildModel({ rtp, hitRate, maxWin, payouts = null }) {
  // With a published paytable, prize sizes are real (each 3-of-a-kind value and its x10 full-screen
  // version); only their probabilities are assumed: weight ∝ x^-a, with `a` solved so E[return] = RTP.
  // Without one, fall back to the generic tiers scaled to the RTP.
  if (payouts?.length) {
    const xs = [...new Set(payouts.flatMap((x) => [x, x * 10]))].filter((x) => x < maxWin).sort((a, b) => a - b);
    const target = rtp / 100 - MAX_WIN_P * maxWin;
    const pHit = hitRate - MAX_WIN_P;
    const mean = (a) => { const w = xs.map((x) => x ** -a); const W = w.reduce((s, v) => s + v, 0); return xs.reduce((s, x, i) => s + (w[i] / W) * pHit * x, 0); };
    let lo = 0, hi = 6;
    for (let i = 0; i < 80; i++) { const mid = (lo + hi) / 2; if (mean(mid) > target) lo = mid; else hi = mid; }
    const a = (lo + hi) / 2;
    const w = xs.map((x) => x ** -a); const W = w.reduce((s, v) => s + v, 0);
    return finish([...xs.map((x, i) => ({ x, p: (w[i] / W) * pHit })), { x: maxWin, p: MAX_WIN_P }], { rtp, hitRate, maxWin, exponent: a });
  }
  const wSum = TIERS.reduce((s, t) => s + t.w, 0);
  const tiers = TIERS.map((t) => ({ x: t.x, p: (t.w / wSum) * (hitRate - MAX_WIN_P) }));
  const fixed = MAX_WIN_P * maxWin;
  const raw = tiers.reduce((s, t) => s + t.p * t.x, 0);
  const k = (rtp / 100 - fixed) / raw; // scale so E[return] = rtp
  return finish([...tiers.map((t) => ({ x: +(t.x * k).toFixed(4), p: t.p })), { x: maxWin, p: MAX_WIN_P }], { rtp, hitRate, maxWin });
}

function finish(outcomes, meta) {
  const cdf = [];
  let acc = 0;
  for (const o of outcomes) { acc += o.p; cdf.push(acc); }
  return { ...meta, outcomes, cdf, expected: outcomes.reduce((s, o) => s + o.p * o.x, 0) };
}

// mulberry32: small seeded PRNG so build output is reproducible.
export function rng(seed) {
  let a = seed >>> 0;
  return () => {
    a = (a + 0x6d2b79f5) >>> 0;
    let t = a;
    t = Math.imul(t ^ (t >>> 15), t | 1);
    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

export function spin(model, r) {
  const u = r();
  for (let i = 0; i < model.cdf.length; i++) if (u < model.cdf[i]) return model.outcomes[i].x;
  return 0;
}

// Many sessions of `spins` spins at `bet`; returns summary stats of net result (won - bet).
export function simulate(model, { sessions = 10000, spins = 1000, bet = 1, seed = 20260927 } = {}) {
  const r = rng(seed);
  const nets = new Float64Array(sessions);
  let wins = 0;
  for (let s = 0; s < sessions; s++) {
    let won = 0;
    for (let i = 0; i < spins; i++) {
      const x = spin(model, r);
      if (x > 0) wins++;
      won += x * bet;
    }
    nets[s] = won - spins * bet;
  }
  const sorted = Array.from(nets).sort((a, b) => a - b);
  const q = (p) => sorted[Math.min(sorted.length - 1, Math.floor(p * sorted.length))];
  return {
    sessions, spins, bet,
    staked: spins * bet,
    lossShare: sorted.filter((n) => n < 0).length / sessions,
    mean: sorted.reduce((s, n) => s + n, 0) / sessions,
    p05: q(0.05), p25: q(0.25), median: q(0.5), p75: q(0.75), p95: q(0.95),
    hitRateObserved: wins / (sessions * spins),
  };
}

// "Horarios": the same model played for 24 simulated hours. Every hour uses the same odds,
// so any difference between hours is chance. Returns per-hour return % and hit rate %.
export function simulateHours(model, { spinsPerHour = 20000, seed = 20260929 } = {}) {
  const r = rng(seed);
  return Array.from({ length: 24 }, (_, hour) => {
    let won = 0, hits = 0;
    for (let i = 0; i < spinsPerHour; i++) { const x = spin(model, r); if (x > 0) hits++; won += x; }
    return { hour, returnPct: (won / spinsPerHour) * 100, hitPct: (hits / spinsPerHour) * 100 };
  });
}

// "Rachas": does a long run without prizes make the next prize more likely?
// Hit rate after `k` consecutive non-winning spins vs. the overall hit rate.
export function streakTest(model, { spins = 2_000_000, ks = [5, 10, 20], seed = 20260929 } = {}) {
  const r = rng(seed);
  const after = new Map(ks.map((k) => [k, { n: 0, hits: 0 }]));
  let dry = 0, hits = 0;
  for (let i = 0; i < spins; i++) {
    const hit = spin(model, r) > 0;
    for (const k of ks) if (dry >= k) { const a = after.get(k); a.n++; if (hit) a.hits++; }
    if (hit) { hits++; dry = 0; } else dry++;
  }
  return { spins, overallPct: (hits / spins) * 100, after: ks.map((k) => ({ k, n: after.get(k).n, hitPct: (after.get(k).hits / after.get(k).n) * 100 })) };
}

// "Martingala": double the bet after each losing spin, back to the base bet after a win.
// Compared with a flat bet over the same number of spins and the same bankroll.
export function martingaleTest(model, { sessions = 10000, spins = 500, base = 1, bankroll = 200, maxBet = 250, seed = 20260929 } = {}) {
  const run = (strategy) => {
    const r = rng(seed);
    const nets = [];
    let busted = 0, wagered = 0;
    for (let s = 0; s < sessions; s++) {
      let cash = bankroll, bet = base;
      for (let i = 0; i < spins; i++) {
        if (bet > cash) { busted++; break; }
        const win = spin(model, r) * bet;
        wagered += bet;
        cash += win - bet;
        if (strategy === 'martingala') bet = win > bet ? base : Math.min(bet * 2, maxBet);
      }
      nets.push(cash - bankroll);
    }
    nets.sort((a, b) => a - b);
    return {
      lossShare: nets.filter((n) => n < 0).length / sessions,
      median: nets[Math.floor(sessions / 2)],
      wagered: wagered / sessions,
      expectedLoss: (wagered / sessions) * (1 - model.rtp / 100),
      p05: nets[Math.floor(0.05 * sessions)],
      p95: nets[Math.floor(0.95 * sessions)],
      bustShare: busted / sessions,
    };
  };
  return { sessions, spins, base, bankroll, flat: run('plana'), martingala: run('martingala') };
}

Contacto y uso de los datos

Puedes citar estas simulaciones si las presentas como simulaciones y enlazas esta página. Para correcciones, preguntas de medios o los datos en bruto: contacto@juegofortunetiger.com.

Fuentes

  1. SlotCatalog – Fortune Tiger (PG Soft). Consultado el .
  2. BigWinBoard – Fortune Tiger (PG Soft) review. Consultado el .
  3. SlotCatalog – Fortune Tiger (Triple Profits Games). Consultado el .
  4. Resultados de Google México para «fortune tiger». Consultado el .