Compare commits

...

2 Commits

Author SHA1 Message Date
HeFanyang 34ce39930c 游戏可以运行 2026-05-24 12:34:49 +08:00
HeFanyang 0670598bf4 游戏可以运行 2026-05-24 10:48:58 +08:00
13 changed files with 163 additions and 50 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+31 -19
View File
@@ -82,36 +82,48 @@ class AIPlayer:
def _attack(self, ai):
actions = []
player = self.battlefield.player
# Frontline units: attack enemy frontline > enemy support > capital
for unit in ai.get_frontline_units():
if not unit.can_attack or unit.has_attacked:
if not unit.can_attack or unit.has_attacked or not ai.can_afford_attack(unit):
continue
enemy_units = player.get_frontline_units()
if enemy_units:
killable = [u for u in enemy_units if u.current_hp <= unit.get_effective_attack()]
enemy_front = player.get_frontline_units()
enemy_support = player.get_support_units()
all_enemy = enemy_front + enemy_support
if all_enemy:
killable = [u for u in all_enemy if u.current_hp <= unit.get_effective_attack()]
if killable:
target = min(killable, key=lambda u: u.current_hp)
else:
target = max(enemy_units, key=lambda u: u.get_effective_attack())
dead = self.battlefield.resolve_attack(unit, target)
actions.append(("attack_unit", unit, target))
else:
self.battlefield.attack_capital(unit)
actions.append(("attack_capital", unit))
# Ranged units in support attack
for unit in ai.get_support_units():
if not unit.can_attack or unit.has_attacked or not unit.is_ranged():
continue
enemy_units = player.get_frontline_units()
if enemy_units:
killable = [u for u in enemy_units if u.current_hp <= unit.get_effective_attack()]
target = min(killable, key=lambda u: u.current_hp) if killable else min(enemy_units, key=lambda u: u.current_hp)
target = max(all_enemy, key=lambda u: u.get_effective_attack())
self.battlefield.resolve_attack(unit, target)
actions.append(("attack_unit", unit, target))
else:
self.battlefield.attack_capital(unit)
actions.append(("attack_capital", unit))
# Support units: attack enemy frontline > enemy support > capital (ranged only)
for unit in ai.get_support_units():
if not unit.can_attack or unit.has_attacked or not ai.can_afford_attack(unit):
continue
enemy_front = player.get_frontline_units()
enemy_support = player.get_support_units()
if unit.is_ranged():
all_enemy = enemy_front + enemy_support
if all_enemy:
killable = [u for u in all_enemy if u.current_hp <= unit.get_effective_attack()]
target = min(killable, key=lambda u: u.current_hp) if killable else min(all_enemy, key=lambda u: u.current_hp)
self.battlefield.resolve_attack(unit, target)
actions.append(("attack_unit", unit, target))
else:
self.battlefield.attack_capital(unit)
actions.append(("attack_capital", unit))
elif enemy_front:
killable = [u for u in enemy_front if u.current_hp <= unit.get_effective_attack()]
target = min(killable, key=lambda u: u.current_hp) if killable else min(enemy_front, key=lambda u: u.current_hp)
self.battlefield.resolve_attack(unit, target)
actions.append(("attack_unit", unit, target))
return actions
def _handle_deploy(self, card, ai):
+6 -1
View File
@@ -79,12 +79,17 @@ class Battlefield:
def resolve_attack(self, attacker, defender):
dead = []
owner = self._get_unit_owner(attacker)
if owner:
attack_cost = owner.get_attack_cost(attacker)
if owner.provisions < attack_cost:
return dead
owner.provisions -= attack_cost
atk = attacker.get_effective_attack()
if "siege" in attacker.abilities and defender == "capital":
atk *= 2
owner = self._get_unit_owner(attacker)
if (owner and owner.faction_id == "han"
and attacker.unit_type == "archer" and defender == "capital"):
atk += 1
+3 -2
View File
@@ -95,9 +95,10 @@ MAX_HAND_SIZE = 8
# --- Game Rules ---
STARTING_CAPITAL_HP = 20
MAX_PROVISIONS = 10
MAX_PROVISIONS = 12
DECK_SIZE = 30
STARTING_HAND_SIZE = 4
FIRST_HAND_SIZE = 4
SECOND_HAND_SIZE = 5
FATIGUE_START_DAMAGE = 1
# --- Rarity ---
+39 -8
View File
@@ -33,6 +33,7 @@ class Game:
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
@@ -112,6 +113,16 @@ class Game:
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 ---
@@ -184,16 +195,30 @@ class Game:
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(4):
for _ in range(hand_size):
self.battlefield.player.draw_card()
self.battlefield.start_game()
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()
self.state = "playing"
if self.player_goes_first:
self.state = "playing"
else:
self._start_ai_turn()
# --- State: Playing ---
@@ -267,11 +292,13 @@ class Game:
opponent = self.battlefield.get_opponent(player)
if unit.zone == "support":
if unit.can_attack and not unit.has_attacked and unit.is_ranged():
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.append(("capital", opponent))
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):
@@ -280,10 +307,11 @@ class Game:
self.ui.selected_unit = unit
self.ui.target_mode = "move"
elif unit.zone == "frontline":
if unit.can_attack and not unit.has_attacked:
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
@@ -414,8 +442,7 @@ class Game:
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
y = FRONTLINE_Y + ZONE_HEIGHT // 2
return x, y
try:
@@ -515,6 +542,8 @@ class Game:
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":
@@ -534,6 +563,8 @@ class Game:
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":
+17 -5
View File
@@ -2,8 +2,9 @@
from card_game.config import (
STARTING_CAPITAL_HP, MAX_PROVISIONS, MAX_HAND_SIZE,
MAX_SUPPORT_SLOTS, MAX_FRONTLINE_SLOTS, DECK_SIZE, STARTING_HAND_SIZE,
MAX_SUPPORT_SLOTS, MAX_FRONTLINE_SLOTS, DECK_SIZE,
FATIGUE_START_DAMAGE, FACTIONS, DECK_PRESETS,
FIRST_HAND_SIZE, SECOND_HAND_SIZE,
)
from card_game.deck import Deck
from card_game.card import Card
@@ -33,14 +34,16 @@ class Player:
preset = DECK_PRESETS[self.faction_id]
self.deck.build(preset["cards"])
def start_game(self):
def start_game(self, hand_size=None):
self.build_deck()
for _ in range(STARTING_HAND_SIZE):
if hand_size is None:
hand_size = FIRST_HAND_SIZE
for _ in range(hand_size):
self.draw_card()
def start_turn(self, turn_number):
self.turn_number = turn_number
gained = min(turn_number + 1, MAX_PROVISIONS)
gained = min(turn_number, MAX_PROVISIONS)
self.provisions = gained
self.max_provisions = gained
@@ -55,7 +58,7 @@ class Player:
unit.reset_turn_flags()
if unit.zone == "frontline" and unit.turn_played < turn_number:
unit.can_attack = True
if unit.zone == "support" and unit.is_ranged() and unit.turn_played < turn_number:
if unit.zone == "support" and unit.turn_played < turn_number:
unit.can_attack = True
# Chu healer
@@ -176,6 +179,15 @@ class Player:
if unit.current_hp < unit.max_hp:
unit.current_hp = min(unit.max_hp, unit.current_hp + 1)
def get_attack_cost(self, unit):
cost = unit.op_cost
if self.faction_id == "yan" and unit.unit_type == "cavalry":
cost = max(0, cost - 1)
return cost
def can_afford_attack(self, unit):
return self.provisions >= self.get_attack_cost(unit)
def cleanup_dead(self):
for i in range(len(self.support_line)):
u = self.support_line[i]
+32 -15
View File
@@ -131,6 +131,30 @@ class UI:
inst = self.font_sm.render("选择你的国家开始游戏", True, INK_WASH_3)
self.screen.blit(inst, (WINDOW_WIDTH // 2 - inst.get_width() // 2, WINDOW_HEIGHT - 60))
def draw_turn_order(self, player_faction):
ink_style.blit_paper_background(self.screen)
ink_style.blit_mountains(self.screen)
faction = FACTIONS[player_faction]
title = self.font_xl.render("选择先后手", True, FACTION_COLORS[player_faction])
self.screen.blit(title, (WINDOW_WIDTH // 2 - title.get_width() // 2, 100))
faction_text = self.font_lg.render(f"你的国家:{faction['name']}", True, PAPER_WHITE)
self.screen.blit(faction_text, (WINDOW_WIDTH // 2 - faction_text.get_width() // 2, 180))
self.menu_buttons = {}
for key, lbl, desc, y in [
("first", "先手", "先行动,起始4张手牌", 280),
("second", "后手", "后行动,起始5张手牌", 370),
]:
btn_w, btn_h = 300, 70
rect = pygame.Rect(WINDOW_WIDTH // 2 - btn_w // 2, y, btn_w, btn_h)
ink_style.draw_ink_rect(self.screen, rect, FACTION_COLORS[player_faction], alpha=200)
pygame.draw.rect(self.screen, PAPER_WHITE, rect, 2, border_radius=5)
t = self.font_lg.render(lbl, True, PAPER_WHITE)
self.screen.blit(t, (rect.centerx - t.get_width() // 2, y + 10))
d = self.font_sm.render(desc, True, LIGHT_GRAY)
self.screen.blit(d, (rect.centerx - d.get_width() // 2, y + 45))
self.menu_buttons[key] = rect
def draw_deck_select(self, player_faction):
ink_style.blit_paper_background(self.screen)
ink_style.blit_mountains(self.screen)
@@ -423,27 +447,23 @@ class UI:
def _draw_frontline(self, battlefield):
y = FRONTLINE_Y
half = ZONE_HEIGHT // 2
# Center divider line
pygame.draw.line(self.screen, INK_WASH_3, (0, y + half), (WINDOW_WIDTH, y + half), 1)
lbl = self.font_sm.render("前 线", True, (ZHU_HONG[0], ZHU_HONG[1], ZHU_HONG[2]))
self.screen.blit(lbl, (WINDOW_WIDTH // 2 - lbl.get_width() // 2, y + 3))
n_fl = MAX_FRONTLINE_SLOTS
uy = y + (ZONE_HEIGHT - FIELD_CARD_HEIGHT) // 2
for i, unit in enumerate(battlefield.ai.frontline):
if unit is None:
continue
ux = _frontline_slot_x(i, n_fl)
uy = y + (half - FIELD_CARD_HEIGHT) // 2
self._draw_field_unit(unit, ux, uy, False, battlefield.ai)
for i, unit in enumerate(battlefield.player.frontline):
if unit is None:
continue
ux = _frontline_slot_x(i, n_fl)
uy = y + half + (half - FIELD_CARD_HEIGHT) // 2
self._draw_field_unit(unit, ux, uy, True, battlefield.player)
def _draw_capital(self, player, zone_y):
@@ -659,10 +679,9 @@ class UI:
ux = _support_slot_x(unit.slot)
uy = zone_y + (zone_h - FIELD_CARD_HEIGHT) // 2
else:
half = ZONE_HEIGHT // 2
n_fl = MAX_FRONTLINE_SLOTS
zone_y = FRONTLINE_Y if is_ai else (FRONTLINE_Y + half)
zone_h = half
zone_y = FRONTLINE_Y
zone_h = ZONE_HEIGHT
ux = _frontline_slot_x(unit.slot, n_fl)
uy = zone_y + (zone_h - FIELD_CARD_HEIGHT) // 2
s = pygame.Surface((FIELD_CARD_WIDTH, FIELD_CARD_HEIGHT), pygame.SRCALPHA)
@@ -681,12 +700,11 @@ class UI:
# Move highlights
if self.target_mode == "move":
half = ZONE_HEIGHT // 2
n_fl = MAX_FRONTLINE_SLOTS
for i, slot in enumerate(battlefield.player.frontline):
if slot is None:
sx = _frontline_slot_x(i, n_fl)
sy = FRONTLINE_Y + half + (half - FIELD_CARD_HEIGHT) // 2
sy = FRONTLINE_Y + (ZONE_HEIGHT - FIELD_CARD_HEIGHT) // 2
s = pygame.Surface((FIELD_CARD_WIDTH, FIELD_CARD_HEIGHT), pygame.SRCALPHA)
s.fill((100, 180, 220, 50))
self.screen.blit(s, (sx, sy))
@@ -717,7 +735,7 @@ class UI:
n_fl = MAX_FRONTLINE_SLOTS
zones = [
(battlefield.player.support_line, PLAYER_SUPPORT_Y, ZONE_HEIGHT, battlefield.player, "support"),
(battlefield.player.frontline, FRONTLINE_Y + half, half, battlefield.player, "frontline"),
(battlefield.player.frontline, FRONTLINE_Y, ZONE_HEIGHT, battlefield.player, "frontline"),
(battlefield.ai.support_line, ENEMY_SUPPORT_Y, ZONE_HEIGHT, battlefield.ai, "support"),
(battlefield.ai.frontline, FRONTLINE_Y, half, battlefield.ai, "frontline"),
]
@@ -750,12 +768,11 @@ class UI:
def get_frontline_slot_at(self, pos, player):
mx, my = pos
half = ZONE_HEIGHT // 2
n_fl = MAX_FRONTLINE_SLOTS
zone_y = FRONTLINE_Y + half
zone_y = FRONTLINE_Y
for i, slot in enumerate(player.frontline):
sx = _frontline_slot_x(i, n_fl)
sy = zone_y + (half - FIELD_CARD_HEIGHT) // 2
sy = zone_y + (ZONE_HEIGHT - FIELD_CARD_HEIGHT) // 2
rect = pygame.Rect(sx, sy, FIELD_CARD_WIDTH, FIELD_CARD_HEIGHT)
if rect.collidepoint(mx, my):
return i
+35
View File
@@ -0,0 +1,35 @@
{
"faction": "qi",
"cards": [
"qi_tongshang",
"qi_tongshang",
"qi_tongshang",
"qi_changgong",
"qi_changgong",
"qi_changgong",
"qi_guanzhong",
"qi_guanzhong",
"qi_jiji",
"qi_jiji",
"qi_jiji",
"qi_shangren",
"qi_shangren",
"qi_shangren",
"qi_sunbin",
"qi_sunbin",
"qi_gongshou",
"qi_gongshou",
"qi_gongshou",
"qi_maobing",
"qi_maobing",
"qi_maobing",
"qi_fuguo",
"qi_tianqi",
"qi_tianqi",
"ally_wu_shuijun",
"ally_wu_shuijun",
"ally_wu_shuijun",
"ally_yue_nvjian",
"ally_yue_nvjian"
]
}