Files
AIGame/Tools/economy_sample.py
T
udxandClaude Opus 5.5 89e0fa246f Add formal enemies, remaining formal partners and synthesized UI SFX
- UI SFX v3: procedurally synthesized one-shots (Tools/synth_ui_sfx.py) wired to
  buttons, rewards, errors and red dots via UiSfx, gated by the SFX setting.
- Five formal enemies (Hornlet, Dashlet, Shellhorn, HornCaptain, GreatHorn):
  Hunyuan 50k -> 2000-tri XPbr, DGXA TokenRig, Idle/Walk/Attack, nested per type
  in PrototypeArena; PrototypeBattle.Enemy gains presentation-only Moving/Strikes.
- Formal partners for all ten pets in the arena and portraits, including new
  VineSprite, ThunderLeopard and TideWhale.
- Pipeline fixes: reject local TokenRig fallback, drop loose mesh islands,
  configurable Unity run_script timeout, companion static stage.
- Also includes concurrent work from another tool in this working tree
  (FlameLion, enemy combat FX, challenge mode, home explorer, server updates).
- Resources registered in Doc/ResDesc_*.md; verified on isolated :18081.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 12:23:55 +08:00

203 lines
8.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()