550 lines
20 KiB
Python
550 lines
20 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._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
|
|
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 and unit.is_ranged():
|
|
self.ui.selected_unit = unit
|
|
self.ui.target_mode = "attack"
|
|
targets = list(opponent.get_frontline_units())
|
|
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.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()
|