from __future__ import annotations import html from typing import Any from game_manager import generate_full_deck Card = dict[str, Any] MANUAL_I18N = { "en": { "title": "How to Play", "kicker": "Manual", "intro": "DOD - Deploy or Draw is a multiplayer UNO-inspired crisis game. Match cards by stack or action type, survive the Director's panic, and complete the final deploy before production melts down.", "objective_title": "Objective", "objective": "Raise Crisis Resolution to 100 before IT Director Panic reaches 100. If panic hits 100, the team loses. If the deploy succeeds, the table wins.", "deck_title": "Deck", "deck_text": "The deck has 108 cards: 100 colored stack cards and 8 wild cards.", "stacks_title": "Stacks", "rules_title": "Basic UNO Rules", "rules": [ "On your turn, play a card that matches the active stack color or the active card category. The category is the symbol/icon in the center of the card, such as bug, revert, meeting, or +2.", "Wild cards can be played on any stack, then you choose the next stack.", "If you cannot or do not want to play, draw a card from the pile.", "After drawing, you may pass your turn when the drawn card is not played.", "The AI player Nemotron is always part of the match and plays by LLM decisions with a local fallback.", ], "deploy_title": "Deploy Shout", "deploy_text": "When you reach one card, shout Deploy. If you forget, another player can accuse you and force a +2 card penalty.", "metrics_title": "Resolution and Panic", "metrics_text": "Good cards increase Resolution or reduce Panic. Risky cards may still help the deploy but can make the Director panic.", "special_title": "Special Cards", "special_text": "Special cards change the flow of the match: skip a player, reverse the direction, force extra draws, or choose the next stack.", "ai_title": "AI Director", "ai_text": "After major plays, the AI Director reacts to the card and the active crisis. If TTS is enabled, the line is also spoken aloud.", "examples_title": "Card Examples", "stack_green": "Frontend", "stack_blue": "Backend", "stack_red": "DevOps", "stack_yellow": "A.I.", "stack_wild": "Wild", "res": "Res", "pan": "Pan", "match": "Match stack or category", "same_stack": "Same stack", "same_category": "Same category", "wild_anytime": "Any stack", "draw_effect": "+2 / +4 pressure", "skip_effect": "Skip next player", "reverse_effect": "Reverse direction", "final_tip": "Tip: keep an eye on both your hand and the crisis meters. The funniest play is not always the safest deploy.", }, "pt": { "title": "Como Jogar", "kicker": "Manual", "intro": "DOD - Deploy or Draw é um jogo multiplayer inspirado em UNO. Combine cartas por stack ou tipo de ação, sobreviva ao pânico do Diretor e conclua o deploy final antes da produção derreter.", "objective_title": "Objetivo", "objective": "Aumente a Resolução da Crise até 100 antes que o Pânico do Diretor de TI chegue a 100. Se o pânico chegar a 100, o time perde. Se o deploy for concluído, a mesa vence.", "deck_title": "Baralho", "deck_text": "O baralho tem 108 cartas: 100 cartas coloridas de stack e 8 cartas coringa.", "stacks_title": "Stacks", "rules_title": "Regras Básicas do UNO", "rules": [ "No seu turno, jogue uma carta que combine com a stack ativa ou com a categoria da carta na mesa. A categoria é o símbolo/ícone no centro da carta, como bug, revert, reunião ou +2.", "Cartas coringa podem ser jogadas em qualquer stack; depois você escolhe a próxima stack.", "Se não puder ou não quiser jogar, compre uma carta do monte.", "Depois de comprar, você pode passar a vez quando a carta comprada não for jogada.", "O jogador de IA Nemotron sempre participa da partida e decide jogadas com LLM, usando fallback local quando necessário.", ], "deploy_title": "Gritar Deploy", "deploy_text": "Quando ficar com uma carta, grite Deploy. Se esquecer, outro jogador pode acusar você e aplicar uma penalidade de +2 cartas.", "metrics_title": "Resolução e Pânico", "metrics_text": "Cartas boas aumentam a Resolução ou reduzem o Pânico. Cartas arriscadas ainda podem ajudar o deploy, mas deixam o Diretor mais desesperado.", "special_title": "Cartas Especiais", "special_text": "Cartas especiais mudam o fluxo da partida: pulam jogador, invertem a direção, forçam compras extras ou escolhem a próxima stack.", "ai_title": "Diretor de IA", "ai_text": "Depois das jogadas importantes, o Diretor de IA reage à carta e à crise ativa. Se o TTS estiver ligado, a frase também é falada em voz alta.", "examples_title": "Exemplos de Cartas", "stack_green": "Frontend", "stack_blue": "Backend", "stack_red": "DevOps", "stack_yellow": "I.A.", "stack_wild": "Coringa", "res": "Res", "pan": "Pan", "match": "Combine stack ou categoria", "same_stack": "Mesma stack", "same_category": "Mesma categoria", "wild_anytime": "Qualquer stack", "draw_effect": "Pressão +2 / +4", "skip_effect": "Pula próximo jogador", "reverse_effect": "Inverte a direção", "final_tip": "Dica: observe sua mão e os medidores da crise. A jogada mais engraçada nem sempre é o deploy mais seguro.", }, } STACK_CLASS = { "green": "manual-card-green", "blue": "manual-card-blue", "red": "manual-card-red", "yellow": "manual-card-yellow", "wild": "manual-card-wild", } STACK_KEYS = { "green": "stack_green", "blue": "stack_blue", "red": "stack_red", "yellow": "stack_yellow", "wild": "stack_wild", } def _escape(value: Any) -> str: """Escape a value for safe HTML output.""" return html.escape(str(value or ""), quote=True) def _localized(value: Any, lang: str) -> str: """Return localized card field text.""" if isinstance(value, dict): return str(value.get(lang) or value.get("en") or "") return str(value or "") def _find_card(deck: list[Card], category: str, stack: str | None = None) -> Card: """Find one representative card by category and optional stack.""" for card in deck: if card.get("category") == category and (stack is None or card.get("stack") == stack): return card raise ValueError(f"Missing manual card sample for {category}") def _render_card(card: Card, lang: str, label: str = "") -> str: """Render a compact static card matching the game board style.""" t = MANUAL_I18N[lang] stack = str(card.get("stack", "wild")) stack_class = STACK_CLASS.get(stack, "manual-card-wild") name = _localized(card.get("name"), lang) symbol = card.get("categorySymbol", "") badge = card.get("badge", "") res = int(card.get("res", 0)) panic = int(card.get("panic", 0)) text_class = "manual-text-dark" if stack == "yellow" else "manual-text-light" stack_label = t.get(STACK_KEYS.get(stack, "stack_wild"), stack.upper()) badge_html = f"
{_escape(badge)}
" if badge else "" label_html = f"
{_escape(label)}
" if label else "" res_prefix = "+" if res >= 0 else "" panic_prefix = "+" if panic >= 0 else "" return ( f"
" f"
" f" {badge_html}" f"
{_escape(stack_label)}
" f"
{_escape(symbol)}
" f"
{_escape(name)}
" f"
" f" {_escape(t['res'])}: {res_prefix}{res}%" f" {_escape(t['pan'])}: {panic_prefix}{panic}%" f"
" f"
" f" {label_html}" f"
" ) def _render_rule_list(rules: list[str]) -> str: """Render manual rules as list items.""" items = "".join(f"
  • {_escape(rule)}
  • " for rule in rules) return f"" def render_how_to_play_html(lang: str = "en") -> str: """Render the localized How to Play page.""" lang = lang if lang in MANUAL_I18N else "en" t = MANUAL_I18N[lang] deck = generate_full_deck() samples = [ (_find_card(deck, "FIX", "green"), t["same_stack"]), (_find_card(deck, "BUG", "blue"), t["same_category"]), (_find_card(deck, "SKIP", "red"), t["skip_effect"]), (_find_card(deck, "REVERSE", "yellow"), t["reverse_effect"]), (_find_card(deck, "ATTACK", "red"), t["draw_effect"]), (_find_card(deck, "WILD"), t["wild_anytime"]), (_find_card(deck, "NUKE"), t["draw_effect"]), ] cards_html = "".join(_render_card(card, lang, label) for card, label in samples) stack_chips = "".join( f"{_escape(t[key])}" for color, key in STACK_KEYS.items() if color != "wild" ) return f"""
    {_escape(t["kicker"])}

    {_escape(t["title"])}

    {_escape(t["intro"])}

    {_escape(t["objective_title"])}

    {_escape(t["objective"])}

    {_escape(t["deck_title"])}

    {_escape(t["deck_text"])}

    {_escape(t["stacks_title"])}

    {stack_chips}

    {_escape(t["metrics_title"])}

    {_escape(t["metrics_text"])}

    {_escape(t["rules_title"])}

    {_render_rule_list(t["rules"])}

    {_escape(t["special_title"])}

    {_escape(t["special_text"])}

    {_escape(t["deploy_title"])}

    {_escape(t["deploy_text"])}

    {_escape(t["ai_title"])}

    {_escape(t["ai_text"])}

    {_escape(t["final_tip"])}
    """