#!/usr/bin/env python3 """G3 economy samples from Config/Gameplay/greenfield.json. Three trajectories from GameDesign 8.2: first day, seven logins, and stuck before a boss. Ignores natural marble recovery. Pinball candy/coins use an equal chance across the five bins. """ from __future__ import annotations import json from pathlib import Path ROOT = Path(__file__).resolve().parents[1] CONFIG = ROOT / "Config/Gameplay/greenfield.json" OUT_MD = ROOT / "Doc/Design/G3/EconomySample.md" OUT_JSON = ROOT / "Doc/Design/G3/economy_sample.json" # GameDesign 8.2, aligned to the live first-five clear and pinball means. DESIGN_DAY1_BALLS = 67 DESIGN_TEN_SHOTS = {"candy": 200, "coins": 520, "tickets": 1} def reward_of(node: dict) -> dict: src = node.get("reward") or node.get("contents") or {} return {k: int(src.get(k, 0) or 0) for k in ("coins", "candy", "balls", "tickets")} def add(total: dict, part: dict, times: int = 1) -> None: for k in total: total[k] += part.get(k, 0) * times def line(name: str, part: dict, times: int = 1) -> str: shown = {k: part.get(k, 0) * times for k in ("coins", "candy", "balls", "tickets") if part.get(k, 0)} body = "、".join(f"{label}{shown[key]}" for key, label in ( ("balls", "弹珠"), ("candy", "糖果"), ("coins", "金币"), ("tickets", "券") ) if key in shown) or "无资源" suffix = f" ×{times}" if times > 1 else "" return f"| {name}{suffix} | {body} |" def main() -> None: cfg = json.loads(CONFIG.read_text(encoding="utf-8")) economy = cfg["economy"] stages = cfg["stages"] pinball = cfg["pinball"] candy_mean = sum(pinball["candy"]) / len(pinball["candy"]) coin_mean = sum(pinball["coins"]) / len(pinball["coins"]) ten = { "candy": int(round(candy_mean * 10)), "coins": int(round(coin_mean * 10)), "tickets": 10 // economy["ticketEvery"], } train_to = lambda level: economy["trainCost"] * sum(range(1, level)) zero = {"coins": 0, "candy": 0, "balls": 0, "tickets": 0} day1 = dict(zero) rows = [] def take(name: str, part: dict, times: int = 1) -> None: add(day1, part, times) rows.append(line(name, part, times)) take("初始弹珠", {"balls": economy["initialBalls"]}) take("欢迎邮件", reward_of(cfg["mail"]["initial"][0])) take("七日签到第1天", reward_of({"reward": cfg["daily"]["signin"][0]})) for task in cfg["daily"]["tasks"]: take(task["title"], reward_of(task)) take(cfg["shop"]["goods"][0]["name"], reward_of(cfg["shop"]["goods"][0])) take("教学目标(通过1-1)", {"balls": economy["goalBalls"]}) first5 = [s for s in stages if s["id"] <= 5] for stage in first5: take(f"首通 {stage['code']}", reward_of(stage)) chest_by_stage = {d["stageId"]: cfg["rewards"]["chests"][d["chest"]] for d in cfg["rewards"]["stageDrops"]} for stage_id in (1, 3): chest = chest_by_stage[stage_id] take(f"打开{chest['name']}({stages[stage_id - 1]['code']}掉落)", reward_of(chest)) design_stage_balls = 17 config_stage_balls = sum(s["reward"]["balls"] for s in first5) design_matched = ( economy["initialBalls"] + cfg["daily"]["signin"][0]["balls"] + sum(t["reward"]["balls"] for t in cfg["daily"]["tasks"]) + cfg["shop"]["goods"][0]["reward"]["balls"] + design_stage_balls ) week = dict(zero) week_rows = [] signin = {"balls": sum(d["balls"] for d in cfg["daily"]["signin"])} tasks = {"balls": 7 * sum(t["reward"]["balls"] for t in cfg["daily"]["tasks"])} shop = {"balls": 7 * cfg["shop"]["goods"][0]["reward"]["balls"]} for name, part in (("七日签到全部领取", signin), ("七日每日任务全部领取", tasks), ("七日免费补给", shop)): add(week, part) week_rows.append(line(name, part)) stuck = dict(zero) stuck_rows = [] def stuck_take(name: str, part: dict, times: int = 1) -> None: add(stuck, part, times) stuck_rows.append(line(name, part, times)) stuck_take("初始弹珠", {"balls": economy["initialBalls"]}) stuck_take("欢迎邮件", reward_of(cfg["mail"]["initial"][0])) stuck_take("签到第1天", reward_of({"reward": cfg["daily"]["signin"][0]})) for task in cfg["daily"]["tasks"]: stuck_take(task["title"], reward_of(task)) stuck_take("免费补给", reward_of(cfg["shop"]["goods"][0])) stuck_take("教学目标", {"balls": economy["goalBalls"]}) for stage in stages: if stage["id"] >= 5: break stuck_take(f"首通 {stage['code']}", reward_of(stage)) stuck_take("打开绿野补给箱", reward_of(chest_by_stage[1])) need_candy = train_to(5) need_coins = economy["evolveCoins"] candy_gap = max(0, need_candy - stuck["candy"]) coin_gap = max(0, need_coins - stuck["coins"]) shots_for_candy = (candy_gap + candy_mean - 1) // candy_mean if candy_mean else 0 shots_for_coins = (coin_gap + coin_mean - 1) // coin_mean if coin_mean else 0 shard_from_chest = 10 shard_gap = economy["evolveShards"] - shard_from_chest report = { "config": str(CONFIG.relative_to(ROOT)).replace("\\", "/"), "ignored": ["natural marble recovery", "pinball path variance beyond bin average"], "pinballAverage": {"candy": candy_mean, "coins": coin_mean, "perTenShots": ten}, "designDay1Balls": DESIGN_DAY1_BALLS, "designFormulaWithLiveNonStageSources": design_matched, "configFirstFiveStageBalls": config_stage_balls, "day1": day1, "sevenDaysWithoutClears": week, "stuckBefore1_5": { "walletBeforePinball": stuck, "trainToLevel5Candy": need_candy, "candyGap": candy_gap, "pinballShotsToCloseCandy": int(shots_for_candy), "evolveCoins": need_coins, "coinGap": coin_gap, "pinballShotsToCloseCoins": int(shots_for_coins), "evolveShards": economy["evolveShards"], "shardsFromChoiceChest": shard_from_chest, "shardGap": shard_gap, }, } OUT_JSON.parent.mkdir(parents=True, exist_ok=True) OUT_JSON.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8") def wallet(w: dict) -> str: return f"弹珠 {w['balls']}、糖果 {w['candy']}、金币 {w['coins']}、捕捉券 {w['tickets']}" md = f"""# G3 经济样例 由 `Tools/economy_sample.py` 读取 `Config/Gameplay/greenfield.json` 生成。不计弹珠自然恢复。弹珠台按五个落区等概率取均值:糖果 {candy_mean:g}、金币 {coin_mean:g}。实际单次受路径影响。 ## 对照设计稿 `GameDesign` 8.2 的首日上限是 **{DESIGN_DAY1_BALLS}** 枚弹珠:初始 20、签到 10、每日任务 15、免费补给 5、前五关首通 {design_stage_balls}。按同一口径合计得到 **{design_matched}**。现行 1-1~1-5 首通弹珠合计是 **{config_stage_balls}**,与稿一致。 稿中「10 发预计糖果 200、金币 520、券 1」。现行均值为糖果 {ten['candy']}、金币 {ten['coins']}、券 {ten['tickets']},与稿一致。 ## 首日获取上限 假设当天领完邮件、签到、三个每日任务、免费补给和教学目标,打通 1-1~1-5 并打开已掉落的绿野补给箱与碎片自选箱。自选箱只给碎片,不进下表钱包。溪口大礼箱要先通过 1-5,打开后另有金币 150、糖果 80、弹珠 5、券 1,未计入本表。 | 来源 | 到账 | | --- | --- | {chr(10).join(rows)} 合计:**{wallet(day1)}**。这是获取上限,不是要求当天花完。 ## 七日回访 不打新关、不发射。每日未领奖励过期,下表按七天都当天领完。签到不循环。 | 来源 | 到账 | | --- | --- | {chr(10).join(week_rows)} 合计:**{wallet(week)}**。 ## 卡在 1-5 之前 已通过 1-1~1-4,打不过溪口大角王。当天可领来源如下,不含为了拿糖果而发射的弹珠台产出。 | 来源 | 到账 | | --- | --- | {chr(10).join(stuck_rows)} 合计:**{wallet(stuck)}**。 - 训练到建议 Lv.5 需要糖果 {need_candy}。账上差 {candy_gap},按均值再发射 {int(shots_for_candy)} 次可补上。主线不另扣体力。 - 进化需要金币 {need_coins}。账上差 {coin_gap},按均值再发射 {int(shots_for_coins)} 次可补上。通过 1-1 后商店每天还能用金币换糖果,那是把金币换成糖果,不增加总资源。 - 进化需要碎片 {economy['evolveShards']}。1-3 自选箱提供 {shard_from_chest},还差 {shard_gap}。重复伙伴每次 {economy['duplicateShards']} 片,不能把碎片换回捕捉券。 训练、进化和再抽一只伙伴都能从免费来源走到,碎片比糖果和金币更慢。 """ OUT_MD.write_text(md, encoding="utf-8") print(json.dumps({"day1": day1, "week": week, "stuck": stuck, "tenShots": ten}, ensure_ascii=False)) if __name__ == "__main__": main()