211 lines
7.2 KiB
Python
211 lines
7.2 KiB
Python
import json
|
|
import uuid
|
|
|
|
from fastapi import FastAPI, HTTPException
|
|
from pydantic import BaseModel
|
|
|
|
from tft import paths
|
|
from tft.model.score import active_trait_tiers
|
|
from tft.sim import economy
|
|
from tft.sim import player as sim_player
|
|
from tft.sim.game import Game, InvalidAction
|
|
|
|
app = FastAPI(title="tft-sim")
|
|
|
|
GAMES: dict[str, Game] = {}
|
|
|
|
|
|
class ActionRequest(BaseModel):
|
|
action: str
|
|
slot: int | None = None
|
|
where: str | None = None
|
|
idx: int | None = None
|
|
item_idx: int | None = None
|
|
board_idx: int | None = None
|
|
choice: int | None = None
|
|
|
|
|
|
@app.get("/health")
|
|
def health() -> dict:
|
|
return {"status": "ok"}
|
|
|
|
|
|
def _load_game_deps():
|
|
from tft.constants.loader import load_constants
|
|
from tft.model import artifact as artifact_mod
|
|
|
|
pointer = paths.latest_static_pointer()
|
|
if not pointer.exists():
|
|
raise HTTPException(503, "no static data: run `tft.cli fetch-static` first")
|
|
set_number = json.loads(pointer.read_text())["set"]
|
|
return artifact_mod.load(set_number), load_constants(set_number)
|
|
|
|
|
|
@app.post("/api/game")
|
|
def new_game(seed: int | None = None) -> dict:
|
|
artifact, cfg = _load_game_deps()
|
|
game_id = uuid.uuid4().hex[:8]
|
|
GAMES[game_id] = Game(artifact, cfg, seed=seed)
|
|
return {"game_id": game_id, **state(game_id)}
|
|
|
|
|
|
@app.get("/api/game/{game_id}")
|
|
def state(game_id: str) -> dict:
|
|
game = GAMES.get(game_id)
|
|
if game is None:
|
|
raise HTTPException(404, "unknown game")
|
|
return serialize(game)
|
|
|
|
|
|
@app.post("/api/game/{game_id}/action")
|
|
def action(game_id: str, req: ActionRequest) -> dict:
|
|
game = GAMES.get(game_id)
|
|
if game is None:
|
|
raise HTTPException(404, "unknown game")
|
|
try:
|
|
if req.action == "buy":
|
|
game.buy(req.slot)
|
|
elif req.action == "sell":
|
|
game.sell(req.where, req.idx)
|
|
elif req.action == "reroll":
|
|
game.reroll()
|
|
elif req.action == "buy_xp":
|
|
game.buy_xp()
|
|
elif req.action == "move":
|
|
game.move(req.where, req.idx)
|
|
elif req.action == "equip":
|
|
game.equip(req.item_idx, req.board_idx)
|
|
elif req.action == "pick_augment":
|
|
game.pick_augment(req.choice)
|
|
elif req.action == "lock":
|
|
game.toggle_lock()
|
|
elif req.action == "next":
|
|
game.step()
|
|
else:
|
|
raise HTTPException(400, f"unknown action {req.action}")
|
|
except InvalidAction as e:
|
|
raise HTTPException(400, str(e))
|
|
except TypeError:
|
|
raise HTTPException(400, f"missing parameters for action {req.action}")
|
|
return serialize(game)
|
|
|
|
|
|
def upgrade_hint(game: Game, api: str) -> int:
|
|
"""0 = kein Upgrade, 2/3 = Kauf ergibt einen 2-/3-Sterner."""
|
|
owned = [u for u in game.board + game.bench
|
|
if u["api_name"] == api and u["stars"] < 3]
|
|
copies = sum(3 ** (u["stars"] - 1) for u in owned)
|
|
if copies + 1 >= 9:
|
|
return 3
|
|
if sum(1 for u in owned if u["stars"] == 1) == 2:
|
|
return 2
|
|
return 0
|
|
|
|
|
|
def unit_view(game: Game, u: dict) -> dict:
|
|
info = game.artifact["static"]["units"][u["api_name"]]
|
|
return {
|
|
"api_name": u["api_name"],
|
|
"name": info.get("name", u["api_name"]),
|
|
"cost": info["cost"],
|
|
"stars": u["stars"],
|
|
"traits": u.get("shown_traits", info["traits"]),
|
|
"icon": info.get("icon"),
|
|
"items": [
|
|
{"api_name": i, "name": game.artifact["static"]["items"].get(i, {}).get("name", i),
|
|
"icon": game.artifact["static"]["items"].get(i, {}).get("icon")}
|
|
for i in u["items"]
|
|
],
|
|
}
|
|
|
|
|
|
def serialize(game: Game) -> dict:
|
|
static = game.artifact["static"]
|
|
tiers = active_trait_tiers(game.board, static["traits"], static["units"])
|
|
counts: dict[str, int] = {}
|
|
for u in game.board:
|
|
for t in set(static["units"][u["api_name"]]["traits"]):
|
|
counts[t] = counts.get(t, 0) + 1
|
|
traits_panel = sorted(
|
|
(
|
|
{
|
|
"api_name": t,
|
|
"name": static["traits"][t].get("name", t),
|
|
"count": n,
|
|
"tier": tiers.get(t, 0),
|
|
"breakpoints": [bp["min_units"] for bp in static["traits"][t]["breakpoints"]],
|
|
"icon": static["traits"][t].get("icon"),
|
|
}
|
|
for t, n in counts.items()
|
|
),
|
|
key=lambda x: (-x["tier"], -x["count"]),
|
|
)
|
|
|
|
stage = game.round["stage"]
|
|
return {
|
|
"round": {"label": game.round["label"], "stage": stage,
|
|
"kind": game.round["kind"]},
|
|
"stage_rounds": [
|
|
{"label": r["label"], "kind": r["kind"], "augment": r["augment"],
|
|
"done": i < game.idx, "current": i == game.idx}
|
|
for i, r in enumerate(game.rounds) if r["stage"] == stage
|
|
],
|
|
"player": {
|
|
"hp": game.hp, "gold": game.gold, "level": game.level, "xp": game.xp,
|
|
"xp_needed": economy.xp_to_next(game.level, game.cfg),
|
|
"streak": game.streak,
|
|
"score": round(game.player_score(), 1),
|
|
},
|
|
"board": [unit_view(game, u) for u in game.board],
|
|
"bench": [unit_view(game, u) for u in game.bench],
|
|
"items": [
|
|
{"api_name": i, "name": static["items"].get(i, {}).get("name", i),
|
|
"icon": static["items"].get(i, {}).get("icon")}
|
|
for i in game.items
|
|
],
|
|
"shop": [
|
|
None if api is None else {
|
|
"api_name": api,
|
|
"name": static["units"][api].get("name", api),
|
|
"cost": static["units"][api]["cost"],
|
|
"traits": [
|
|
{"name": static["traits"][t].get("name", t),
|
|
"icon": static["traits"][t].get("icon")}
|
|
for t in static["units"][api]["traits"] if t in static["traits"]
|
|
],
|
|
"icon": static["units"][api].get("icon"),
|
|
"upgrade": upgrade_hint(game, api),
|
|
}
|
|
for api in game.shop
|
|
],
|
|
"shop_locked": game.shop_locked,
|
|
"shop_odds": game.cfg["shop"]["odds"][game.level - 1],
|
|
"traits": traits_panel,
|
|
"augment_offer": [
|
|
{"api_name": a, "name": static["augments"].get(a, {}).get("name", a),
|
|
"icon": static["augments"].get(a, {}).get("icon")}
|
|
for a in game.augment_offer
|
|
],
|
|
"augments": [static["augments"].get(a, {}).get("name", a) for a in game.augments],
|
|
"opponents": sorted(
|
|
(
|
|
{
|
|
"name": b.name, "archetype": b.archetype, "hp": max(b.hp, 0),
|
|
"alive": b.alive, "placement": b.placement,
|
|
"level": b.level if b.alive else None,
|
|
"gold": b.gold if b.alive else None,
|
|
"streak": b.streak if b.alive else None,
|
|
"score": round(sim_player.score(b, game.artifact), 1) if b.alive else None,
|
|
"board": [unit_view(game, u) for u in b.board],
|
|
}
|
|
for b in game.bots
|
|
),
|
|
key=lambda x: -x["hp"],
|
|
),
|
|
"reroll_cost": game.cfg["shop"]["reroll_cost"],
|
|
"xp_cost": game.cfg["xp"]["buy_cost"],
|
|
"over": game.over,
|
|
"placement": game.placement,
|
|
"log": game.log[-8:],
|
|
}
|