"""Game class: main loop, state machine, event handling.""" import sys import os import json import random import pygame from card_game.config import WINDOW_WIDTH, WINDOW_HEIGHT, FPS, TENG_HUANG, INK_WASH_3 from card_game.battlefield import Battlefield from card_game.ui import UI from card_game.ai import AIPlayer from card_game.factions import apply_faction_passive from card_game import ink_style SAVED_DECKS_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "saved_decks") class Game: def __init__(self): pygame.init() self.screen = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT)) pygame.display.set_caption("战国卡牌 - 水墨风云") self.clock = pygame.time.Clock() # Initialize ink style cache ink_style.init_cache() self.ui = UI(self.screen) self.ai_player = None self.state = "menu" self.battlefield = None self.player_faction = None self.ai_faction = None self.custom_deck = None self._load_custom_deck() self.custom_deck = None # AI turn timing self.ai_timer = 0 self.ai_step = 0 def _load_custom_deck(self): if not self.player_faction: return path = os.path.join(SAVED_DECKS_DIR, f"{self.player_faction}.json") if os.path.exists(path): try: with open(path, "r", encoding="utf-8") as f: data = json.load(f) if len(data.get("cards", [])) == 30: self.custom_deck = data["cards"] except Exception: self.custom_deck = None def _save_custom_deck(self, faction_id, cards): os.makedirs(SAVED_DECKS_DIR, exist_ok=True) path = os.path.join(SAVED_DECKS_DIR, f"{faction_id}.json") try: with open(path, "w", encoding="utf-8") as f: json.dump({"faction": faction_id, "cards": cards}, f, ensure_ascii=False, indent=2) self.custom_deck = list(cards) except Exception: pass def run(self): while True: try: for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() sys.exit() elif event.type == pygame.MOUSEBUTTONDOWN: if event.button == 1: self._handle_click(event.pos) elif event.button == 3: self._handle_right_click(event.pos) elif event.type == pygame.MOUSEMOTION: self._handle_hover(event.pos) elif event.type == pygame.KEYDOWN: if event.key == pygame.K_ESCAPE: self._handle_escape() self._update() self._draw() pygame.display.flip() self.clock.tick(FPS) except Exception as e: import traceback traceback.print_exc() self.screen.fill((0, 0, 0)) font = pygame.font.SysFont("microsoftyahei", 16) lines = traceback.format_exc().split('\n') for i, line in enumerate(lines[:15]): surf = font.render(line[:80], True, (255, 80, 80)) self.screen.blit(surf, (10, 10 + i * 20)) pygame.display.flip() waiting = True while waiting: for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() sys.exit() if event.type == pygame.KEYDOWN: waiting = False # --- State: Menu --- def _handle_click_menu(self, pos): fid = self.ui.get_faction_at(pos) if fid: self.player_faction = fid self.custom_deck = None self._load_custom_deck() self.state = "deck_select" # --- State: Deck Select --- def _handle_click_deck_select(self, pos): if "back" in self.ui.menu_buttons and self.ui.menu_buttons["back"].collidepoint(pos): self.state = "menu" return if "deck_build" in self.ui.menu_buttons and self.ui.menu_buttons["deck_build"].collidepoint(pos): self.ui.deck_builder_cards = [] self.ui.deck_builder_faction = self.player_faction self.state = "deck_build" return if "random" in self.ui.menu_buttons and self.ui.menu_buttons["random"].collidepoint(pos): from card_game.config import FACTIONS others = [f for f in FACTIONS.keys() if f != self.player_faction] self.ai_faction = random.choice(others) self._start_game() return fid = self.ui.get_faction_at(pos) if fid and fid != self.player_faction: self.ai_faction = fid self._start_game() # --- State: Deck Build --- def _handle_click_deck_build(self, pos): from card_game.config import DECK_SIZE, RARITY_LIMITS, CARD_DATABASE if "back" in self.ui.menu_buttons and self.ui.menu_buttons["back"].collidepoint(pos): self.state = "deck_select" return if "clear" in self.ui.menu_buttons and self.ui.menu_buttons["clear"].collidepoint(pos): self.ui.deck_builder_cards = [] return if "preset" in self.ui.menu_buttons and self.ui.menu_buttons["preset"].collidepoint(pos): from card_game.config import DECK_PRESETS self.ui.deck_builder_cards = list(DECK_PRESETS[self.player_faction]["cards"]) return if "confirm" in self.ui.menu_buttons and self.ui.menu_buttons["confirm"].collidepoint(pos): if len(self.ui.deck_builder_cards) == DECK_SIZE: self._save_custom_deck(self.player_faction, self.ui.deck_builder_cards) self.state = "deck_select" return cid = self.ui.get_deck_card_at(pos) if cid: card_data = CARD_DATABASE[cid] in_deck = self.ui.deck_builder_cards.count(cid) max_copies = RARITY_LIMITS.get(card_data["rarity"], 3) if in_deck < max_copies and len(self.ui.deck_builder_cards) < DECK_SIZE: self.ui.deck_builder_cards.append(cid) return cid = self.ui.get_deck_build_card_at(pos) if cid: card_data = CARD_DATABASE[cid] in_deck = self.ui.deck_builder_cards.count(cid) max_copies = RARITY_LIMITS.get(card_data["rarity"], 3) if in_deck < max_copies and len(self.ui.deck_builder_cards) < DECK_SIZE: self.ui.deck_builder_cards.append(cid) def _handle_right_click(self, pos): if self.state != "deck_build": return cid = self.ui.get_deck_card_at(pos) if cid and cid in self.ui.deck_builder_cards: self.ui.deck_builder_cards.remove(cid) return cid = self.ui.get_deck_build_card_at(pos) if cid and cid in self.ui.deck_builder_cards: self.ui.deck_builder_cards.remove(cid) def _start_game(self): self.battlefield = Battlefield(self.player_faction, self.ai_faction) if self.custom_deck: self.battlefield.player.deck.build(self.custom_deck) self.battlefield.player.hand = [] for _ in range(4): self.battlefield.player.draw_card() self.battlefield.start_game() self.ai_player = AIPlayer(self.battlefield) self.ui.clear_selection() self.state = "playing" # --- State: Playing --- def _handle_click_playing(self, pos): bf = self.battlefield player = bf.player if self.ui.end_turn_btn.collidepoint(pos): self.ui.clear_selection() self._start_ai_turn() return if self.ui.target_mode == "order_target" and self.ui.selected_card: self._handle_order_target_click(pos) return if self.ui.target_mode == "deploy" and self.ui.selected_card: if self._handle_deploy_click(pos): return self.ui.clear_selection() return if self.ui.target_mode == "move" and self.ui.selected_unit: self._handle_move_click(pos) return if self.ui.selected_unit and self.ui.target_mode == "attack": self._handle_attack_click(pos) return card = self.ui.get_hand_card_at(pos, player) if card: self._select_hand_card(card) return unit, owner = self.ui.get_field_unit_at(pos, bf) if unit and owner == player: self._select_own_unit(unit) return self.ui.clear_selection() def _select_hand_card(self, card): player = self.battlefield.player self.ui.clear_selection() if not player.can_play_card(card): return if card.card_type == "unit": self.ui.selected_card = card self.ui.target_mode = "deploy" self.ui.valid_targets = [] elif card.card_type == "order": if self.battlefield.needs_target(card): self.ui.selected_card = card self.ui.target_mode = "order_target" self.ui.valid_targets = self.battlefield.get_valid_targets(card, player) else: self.battlefield.apply_order_effect(card, player) player.play_order(card) self.ui.clear_selection() def _select_own_unit(self, unit): self.ui.clear_selection() player = self.battlefield.player owner = self.battlefield._get_unit_owner(unit) if owner != player: return opponent = self.battlefield.get_opponent(player) if unit.zone == "support": if unit.can_attack and not unit.has_attacked: self.ui.selected_unit = unit self.ui.target_mode = "attack" targets = list(opponent.get_frontline_units()) targets.extend(opponent.get_support_units()) if unit.is_ranged(): targets.append(("capital", opponent)) self.ui.valid_targets = targets return if self.battlefield.can_move_to_frontline(player): op_cost = player._get_op_cost(unit) if player.provisions >= op_cost: self.ui.selected_unit = unit self.ui.target_mode = "move" elif unit.zone == "frontline": if unit.can_attack and not unit.has_attacked: self.ui.selected_unit = unit self.ui.target_mode = "attack" targets = list(opponent.get_frontline_units()) targets.extend(opponent.get_support_units()) targets.append(("capital", opponent)) self.ui.valid_targets = targets def _handle_deploy_click(self, pos): from card_game.config import PLAYER_SUPPORT_Y, ZONE_HEIGHT player = self.battlefield.player card = self.ui.selected_card if not card: return False mx, my = pos in_support_zone = (PLAYER_SUPPORT_Y <= my <= PLAYER_SUPPORT_Y + ZONE_HEIGHT) if in_support_zone: slot = self.ui.get_support_slot_at(pos, player) if slot < 0: for i, s in enumerate(player.support_line): if s is None: slot = i break if slot >= 0: apply_faction_passive(card, player.faction_id) player.deploy_unit(card, slot) self._handle_deploy_abilities(card, player) self.ui.clear_selection() return True return False def _handle_order_target_click(self, pos): card = self.ui.selected_card player = self.battlefield.player if not card: self.ui.clear_selection() return unit, owner = self.ui.get_field_unit_at(pos, self.battlefield) if unit and unit in self.ui.valid_targets: self.battlefield.apply_order_effect(card, player, unit) player.play_order(card) self.ui.clear_selection() return if self.ui.get_enemy_capital_at(pos, self.battlefield.ai): self.battlefield.apply_order_effect(card, player, "capital") player.play_order(card) self.ui.clear_selection() def _handle_move_click(self, pos): player = self.battlefield.player unit = self.ui.selected_unit if not unit: self.ui.clear_selection() return if not self.battlefield.can_move_to_frontline(player): self.ui.clear_selection() return slot = self.ui.get_frontline_slot_at(pos, player) if slot >= 0: result_slot = player.move_to_frontline(unit) if result_slot >= 0: if self.battlefield.frontline_controller is None: self.battlefield.claim_frontline(player) self.ui.clear_selection() else: self.ui.clear_selection() def _handle_attack_click(self, pos): unit = self.ui.selected_unit if not unit: self.ui.clear_selection() return bf = self.battlefield player = bf.player opponent = bf.ai valid = self.ui.valid_targets target_unit, target_owner = self.ui.get_field_unit_at(pos, bf) if target_unit and target_owner == opponent and target_unit in valid: dead = bf.resolve_attack(unit, target_unit) self._add_attack_effect(unit, target_unit) self.ui.clear_selection() return if self.ui.get_enemy_capital_at(pos, opponent): cap_target = ("capital", opponent) if cap_target in valid: bf.attack_capital(unit) self.ui.clear_selection() return self.ui.clear_selection() def _handle_deploy_abilities(self, card, player): for ability in card.abilities: if ability.startswith("draw_on_deploy:"): count = int(ability.split(":")[1]) for _ in range(count): player.draw_card() elif ability.startswith("damage_on_deploy:"): dmg = int(ability.split(":")[1]) import random opponent = self.battlefield.get_opponent(player) targets = opponent.get_all_units() if targets: target = random.choice(targets) target.take_damage(dmg) elif ability.startswith("gain_on_deploy:"): amount = int(ability.split(":")[1]) player.provisions += amount def _add_attack_effect(self, attacker, target): from card_game.ui import _support_slot_x, _frontline_slot_x from card_game.config import (FIELD_CARD_WIDTH, FIELD_CARD_HEIGHT, ZONE_HEIGHT, ENEMY_SUPPORT_Y, FRONTLINE_Y, PLAYER_SUPPORT_Y, WINDOW_WIDTH, MAX_FRONTLINE_SLOTS, CAPITAL_WIDTH) half = ZONE_HEIGHT // 2 n_fl = MAX_FRONTLINE_SLOTS def _unit_pos(unit): owner = self.battlefield._get_unit_owner(unit) is_ai = (owner == self.battlefield.ai) if unit.zone == "support": x = _support_slot_x(unit.slot) + FIELD_CARD_WIDTH // 2 zone_y = ENEMY_SUPPORT_Y if is_ai else PLAYER_SUPPORT_Y y = zone_y + ZONE_HEIGHT // 2 else: x = _frontline_slot_x(unit.slot, n_fl) + FIELD_CARD_WIDTH // 2 zone_y = FRONTLINE_Y if is_ai else (FRONTLINE_Y + half) y = zone_y + half // 2 return x, y try: if hasattr(attacker, 'zone'): ax, ay = _unit_pos(attacker) else: ax, ay = WINDOW_WIDTH // 2, ENEMY_SUPPORT_Y + ZONE_HEIGHT // 2 if hasattr(target, 'zone'): tx, ty = _unit_pos(target) else: tx = WINDOW_WIDTH // 2 ty = ENEMY_SUPPORT_Y + ZONE_HEIGHT // 2 self.ui.effects.add_attack_line(ax, ay, tx, ty) self.ui.effects.add_damage(tx, ty - 20, attacker.get_effective_attack()) except Exception: pass # --- State: AI Turn --- def _start_ai_turn(self): self.state = "ai_turn" try: self.battlefield.start_ai_turn() self.ai_actions = self.ai_player.execute_turn() except Exception as e: import traceback traceback.print_exc() self.ai_actions = [] self.ai_step = 0 self.ai_timer = 20 def _update_ai_turn(self): if self.ai_timer > 0: self.ai_timer -= 1 return if self.ai_step >= len(self.ai_actions): try: self.battlefield.end_ai_turn() except Exception: pass if self.battlefield.game_over: self.state = "game_over" else: self.state = "playing" return action = self.ai_actions[self.ai_step] self.ai_step += 1 self.ai_timer = 15 if action[0] in ("attack_unit", "attack_capital"): attacker = action[1] try: if len(action) > 2: self._add_attack_effect(attacker, action[2]) else: self._add_attack_effect(attacker, "capital") except Exception: pass # --- Hover --- def _handle_hover(self, pos): if self.state not in ("playing", "ai_turn"): return if not self.battlefield: return player = self.battlefield.player card = self.ui.get_hand_card_at(pos, player) self.ui.hover_card = card unit, owner = self.ui.get_field_unit_at(pos, self.battlefield) self.ui.hover_field_unit = unit self.ui.hover_pos = pos # --- Escape --- def _handle_escape(self): self.ui.clear_selection() if self.state in ("playing", "ai_turn"): self.state = "menu" self.battlefield = None # --- State: Game Over --- def _handle_click_game_over(self, pos): if "restart" in self.ui.menu_buttons and self.ui.menu_buttons["restart"].collidepoint(pos): self._start_game() elif "menu" in self.ui.menu_buttons and self.ui.menu_buttons["menu"].collidepoint(pos): self.state = "menu" self.battlefield = None # --- Unified handlers --- def _handle_click(self, pos): if self.state == "menu": self._handle_click_menu(pos) elif self.state == "deck_select": self._handle_click_deck_select(pos) elif self.state == "deck_build": self._handle_click_deck_build(pos) elif self.state == "playing": self._handle_click_playing(pos) elif self.state == "game_over": self._handle_click_game_over(pos) def _update(self): if self.state == "ai_turn": self._update_ai_turn() if self.battlefield: self.battlefield.update_effects() self.ui.effects.update() def _draw(self): if self.state == "menu": self.ui.draw_menu() elif self.state == "deck_select": self.ui.draw_deck_select(self.player_faction) elif self.state == "deck_build": self.ui.draw_deck_builder(self.player_faction) elif self.state in ("playing", "ai_turn"): self.ui.draw_game(self.battlefield) if self.state == "ai_turn": from card_game.config import WINDOW_WIDTH, ENEMY_INFO_HEIGHT text = self.ui.font_md.render("对手回合...", True, INK_WASH_3) self.screen.blit(text, (WINDOW_WIDTH // 2 - text.get_width() // 2, ENEMY_INFO_HEIGHT + 55)) elif self.state == "game_over": self.ui.draw_game(self.battlefield) self.ui.draw_game_over(self.battlefield.winner, self.battlefield) if __name__ == "__main__": game = Game() game.run()