Compare commits

...

3 Commits

Author SHA1 Message Date
3df3e4f560 游戏可以运行 2026-05-24 12:40:57 +08:00
34ce39930c 游戏可以运行 2026-05-24 12:34:49 +08:00
0670598bf4 游戏可以运行 2026-05-24 10:48:58 +08:00
13 changed files with 166 additions and 51 deletions

View File

@@ -82,36 +82,49 @@ class AIPlayer:
def _attack(self, ai):
actions = []
player = self.battlefield.player
# Frontline units: non-archer attacks enemy support only, archer attacks all
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()]
if unit.is_ranged():
targets = player.get_frontline_units() + player.get_support_units()
else:
targets = player.get_support_units()
if targets:
killable = [u for u in targets 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(targets, 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):

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

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 ---

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()
if self.player_goes_first:
self.state = "playing"
else:
self._start_ai_turn()
# --- State: Playing ---
@@ -267,10 +292,12 @@ 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())
if unit.is_ranged():
targets.extend(opponent.get_support_units())
targets.append(("capital", opponent))
self.ui.valid_targets = targets
return
@@ -280,11 +307,13 @@ 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 = list(opponent.get_support_units())
targets.append(("capital", opponent))
if unit.is_ranged():
targets.extend(opponent.get_frontline_units())
self.ui.valid_targets = targets
def _handle_deploy_click(self, pos):
@@ -414,8 +443,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 +543,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 +564,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":

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]

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
saved_decks/qi.json Normal file
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"
]
}