Compare commits
3 Commits
a8c5fb2356
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 3df3e4f560 | |||
| 34ce39930c | |||
| 0670598bf4 |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ---
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
35
saved_decks/qi.json
Normal 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"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user