Spaces:
Running
Running
Download grindscale/model.js from CyberMax-tools/cybermax: direct link, hf CLI and curl.
- Browser
- Download file 4.16 kB
-
https://huggingface.co/spaces/CyberMax-tools/cybermax/resolve/main/grindscale/model.js
- Command line
-
hf download hf://spaces/CyberMax-tools/cybermax/grindscale/model.js
-
curl -L -o model.js https://huggingface.co/spaces/CyberMax-tools/cybermax/resolve/main/grindscale/model.js
4.16 kB
| /* Grindscale model (browser). Same maths as src/model.py and the workbook. © 2026 CyberMax. All rights reserved. */ | |
| (function (root) { | |
| "use strict"; | |
| var CURVES = ["Linear", "Power", "Exponential", "Blend"]; | |
| var DEFAULTS = { max_level: 50, curve: "Blend", base_xp: 100, linear_step: 50, power_exp: 1.6, exp_growth: 1.06, | |
| blend_weight: 0.25, round_to: 10, xph_l1: 1550, xph_growth: 1.07, target_hours: 60, sessions_per_day: 2, minutes_per_session: 20 }; | |
| var PRESETS = { | |
| "Mobile casual (30 levels)": { max_level: 30, curve: "Power", base_xp: 50, linear_step: 25, power_exp: 1.4, exp_growth: 1.08, | |
| blend_weight: 0.2, round_to: 5, xph_l1: 1150, xph_growth: 1.08, target_hours: 15, sessions_per_day: 3, minutes_per_session: 8 }, | |
| "Classic RPG (50 levels)": DEFAULTS, | |
| "Long-haul RPG (100 levels)": { max_level: 100, curve: "Blend", base_xp: 200, linear_step: 80, power_exp: 1.7, exp_growth: 1.035, | |
| blend_weight: 0.3, round_to: 50, xph_l1: 2350, xph_growth: 1.05, target_hours: 300, sessions_per_day: 2, minutes_per_session: 45 }, | |
| "Roguelite meta (20 ranks)": { max_level: 20, curve: "Exponential", base_xp: 300, linear_step: 150, power_exp: 1.5, exp_growth: 1.15, | |
| blend_weight: 0.5, round_to: 25, xph_l1: 1050, xph_growth: 1.0, target_hours: 25, sessions_per_day: 2, minutes_per_session: 30 } | |
| }; | |
| function xround(x, digits) { | |
| var m = Math.pow(10, digits || 0); | |
| var v = Math.floor(Math.abs(x) * m + 0.5 + 1e-9) / m; | |
| return x < 0 ? -v : v; | |
| } | |
| function rawXp(s, lv) { | |
| var pw = s.base_xp * Math.pow(lv, s.power_exp); | |
| var ex = s.base_xp * Math.pow(s.exp_growth, lv - 1); | |
| switch (s.curve) { | |
| case "Linear": return s.base_xp + s.linear_step * (lv - 1); | |
| case "Power": return pw; | |
| case "Exponential": return ex; | |
| default: return (1 - s.blend_weight) * pw + s.blend_weight * ex; | |
| } | |
| } | |
| function xpTable(s) { | |
| var rows = [], totalXp = 0, totalH = 0; | |
| for (var lv = 1; lv <= s.max_level; lv++) { | |
| var need = lv === s.max_level ? 0 : Math.max(s.round_to, xround(rawXp(s, lv) / s.round_to) * s.round_to); | |
| var xph = s.xph_l1 * Math.pow(s.xph_growth, lv - 1); | |
| var hours = need / xph; | |
| rows.push({ level: lv, xp_to_next: need, total_xp: totalXp, xp_per_hour: xph, hours: hours, total_hours: totalH }); | |
| totalXp += need; | |
| totalH += hours; | |
| } | |
| return rows; | |
| } | |
| function summary(s) { | |
| var rows = xpTable(s), last = rows[rows.length - 1]; | |
| var ratio = last.total_hours / s.target_hours; | |
| var verdict = ratio > 1.15 ? "Slower than target: lower base XP or growth, or raise XP per hour" | |
| : ratio < 0.85 ? "Faster than target: raise XP needs or slow XP per hour" : "On target (within 15%)"; | |
| return { total_xp: last.total_xp, total_hours: last.total_hours, ratio: ratio, verdict: verdict, | |
| hours_to_10: rows[Math.min(10, s.max_level) - 1].total_hours, | |
| hours_to_half: rows[Math.ceil(s.max_level / 2) - 1].total_hours, | |
| days_to_max: last.total_hours / (s.sessions_per_day * s.minutes_per_session / 60) }; | |
| } | |
| /* Drop chance maths: p per try, pity = guaranteed on try N (optional). */ | |
| function drop(p, pity, perHour) { | |
| function tries(q) { return p >= 1 ? 1 : Math.ceil(Math.log(1 - q) / Math.log(1 - p) - 1e-9); } | |
| var t90 = tries(0.9); | |
| return { chance: p, one_in: 1 / p, tries50: tries(0.5), tries90: t90, tries99: tries(0.99), | |
| hours90: perHour > 0 ? t90 / perHour : null, per_hour: p * (perHour || 0), | |
| expected: pity ? (1 - Math.pow(1 - p, pity)) / p : 1 / p, | |
| within: function (n) { return pity && n >= pity ? 1 : 1 - Math.pow(1 - p, n); } }; | |
| } | |
| function csv(s) { | |
| var lines = ["level,xp_to_next,total_xp,xp_per_hour"]; | |
| xpTable(s).forEach(function (r) { lines.push([r.level, r.xp_to_next, r.total_xp, num(xround(r.xp_per_hour, 2))].join(",")); }); | |
| return lines.join("\n") + "\n"; | |
| } | |
| function num(v) { return String(v); } | |
| root.Grindscale = { CURVES: CURVES, DEFAULTS: DEFAULTS, PRESETS: PRESETS, xround: xround, rawXp: rawXp, xpTable: xpTable, | |
| summary: summary, drop: drop, csv: csv }; | |
| })(typeof window !== "undefined" ? window : globalThis); | |