Files
card/card_game/main.py
2026-05-24 12:34:49 +08:00

587 lines
22 KiB
Python

"""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.player_goes_first = True
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 = "turn_order"
# --- State: Turn Order ---
def _handle_click_turn_order(self, pos):
if "first" in self.ui.menu_buttons and self.ui.menu_buttons["first"].collidepoint(pos):
self.player_goes_first = True
self.state = "deck_select"
elif "second" in self.ui.menu_buttons and self.ui.menu_buttons["second"].collidepoint(pos):
self.player_goes_first = False
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):
from card_game.config import FIRST_HAND_SIZE, SECOND_HAND_SIZE
self.battlefield = Battlefield(self.player_faction, self.ai_faction)
if self.player_goes_first:
self.battlefield.player.start_game(FIRST_HAND_SIZE)
self.battlefield.ai.start_game(SECOND_HAND_SIZE)
else:
self.battlefield.player.start_game(SECOND_HAND_SIZE)
self.battlefield.ai.start_game(FIRST_HAND_SIZE)
if self.custom_deck:
self.battlefield.player.deck.build(self.custom_deck)
hand_size = FIRST_HAND_SIZE if self.player_goes_first else SECOND_HAND_SIZE
self.battlefield.player.hand = []
for _ in range(hand_size):
self.battlefield.player.draw_card()
self.battlefield.turn_number = 1
self.battlefield.frontline_controller = None
self.battlefield.current_turn = "player"
self.battlefield.player.start_turn(1)
self.ai_player = AIPlayer(self.battlefield)
self.ui.clear_selection()
if self.player_goes_first:
self.state = "playing"
else:
self._start_ai_turn()
# --- 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 and player.can_afford_attack(unit):
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 and player.can_afford_attack(unit):
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
y = FRONTLINE_Y + ZONE_HEIGHT // 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 == "turn_order":
self._handle_click_turn_order(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 == "turn_order":
self.ui.draw_turn_order(self.player_faction)
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()