From 191d32bea227a72174b2feb31733e5868fb15fcd Mon Sep 17 00:00:00 2001 From: legonzaur Date: Thu, 2 May 2024 23:10:04 +0200 Subject: [PATCH] fixes #1; Migrate card activation effect to factory in card_behaviour.py --- src/model/card.py | 14 ++- src/model/card_behaviour.py | 214 ++++++++++++++++++++++++++++++++++++ src/model/card_loader.py | 28 +++-- src/model/turn.py | 167 ++++++---------------------- 4 files changed, 275 insertions(+), 148 deletions(-) create mode 100644 src/model/card_behaviour.py diff --git a/src/model/card.py b/src/model/card.py index 8ba6ef9..bfa1276 100644 --- a/src/model/card.py +++ b/src/model/card.py @@ -1,7 +1,10 @@ -from typing import List, Optional +from typing import List, Optional, Callable, TYPE_CHECKING, Union from model import Serializable, CardRole, CardType, CardFaction from model.card_validator import CardEffects, EffectType, EffectTimes +if TYPE_CHECKING: + from model import Player, Turn + class Effect(Serializable): """Represents an effect of a card (gold, damage, heal, draw a card...)""" @@ -13,12 +16,14 @@ class Effect(Serializable): mutex: List["Effect"] effect_type: Optional[EffectType] = None times: Optional[EffectTimes] = None - card: Optional["Card"] = None + card: "Card" + activate: Callable[["Turn"], Union["Player", "Card"]] def __init__( self, effect: CardEffects, amount: int, + activate: Callable[["Turn"], Union["Player", "Card"]], sub_effects: Optional[List["Effect"]] = None, effect_type: Optional[EffectType] = None, times: Optional[EffectTimes] = None, @@ -31,18 +36,21 @@ class Effect(Serializable): sub_effects = [] self.effect = effect self.amount = amount + self.activate = activate self.sub_effects = sub_effects self.effect_type = effect_type self.mutex = mutex self.times = times def serialize(self) -> dict: - return super().serialize() | { + data = super().serialize() | { "sub_effects": [effect.uuid for effect in self.sub_effects], "mutex": [effect.uuid for effect in self.mutex], "card": self.card.uuid, } + return {k: data[k] for k in data.keys() - ["activate"]} + class Card(Serializable): """Represents any playable card""" diff --git a/src/model/card_behaviour.py b/src/model/card_behaviour.py new file mode 100644 index 0000000..cb5676c --- /dev/null +++ b/src/model/card_behaviour.py @@ -0,0 +1,214 @@ +from typing import TYPE_CHECKING, Awaitable, Optional +from model.card_validator import EffectParser +from model.card import Card, Effect, EffectType, CardEffects, EffectTimes, CardType + + +if TYPE_CHECKING: + from model import Turn + + +# Effect Types +async def effect_type_suicide(effect: Effect, turn: "Turn"): + for e in effect.card.effects: + if e != effect: + while e in turn.effects_availables: + turn.effects_availables.remove(e) + for se in e.sub_effects: + if se != effect: + while se in turn.effects_availables: + turn.effects_availables.remove(se) + if effect.card in turn.player.board: + await turn.player.sacrifice_card(str(effect.card.uuid)) + return True + + +async def effect_type_faction_combo(effect: Effect, turn: "Turn"): + for c in turn.cards_locked: + if ( + c in turn.cards_locked + and c.faction == effect.card.faction + and c != effect.card + ): + return True + return False + + +effect_types_all_behaviours = { + EffectType.SUICIDE: effect_type_suicide, + EffectType.FACTION_COMBO: effect_type_faction_combo, +} + + +# Card Effects +async def effect_gold(effect: Effect, turn: "Turn", _target: str): + turn.gold_reserve += effect.amount + return True + + +async def effect_damage(effect: Effect, turn: "Turn", _target: str): + turn.damage_reserve += effect.amount + return True + + +async def effect_heal(effect: Effect, turn: "Turn", _target: str): + turn.heal_reserve += effect.amount + return True + + +def effect_prepare(_effect: Effect, turn: "Turn", target: str): + return turn.prepare(target) + + +async def effect_stun(_effect: Effect, turn: "Turn", target: str): + return await turn.player.stun(target) + + +async def effect_sacrifice(_effect: Effect, turn: "Turn", target: str): + return await turn.sacrifice_card(target) + + +async def effect_draw(_effect: Effect, turn: "Turn", _target: str): + return await turn.player.draw(1) + + +async def effect_draw_and_discard(_effect: Effect, turn: "Turn", _target: str): + turn.discard_markers += 1 + return await turn.player.draw(1) + + +async def effect_discard(_effect: Effect, turn: "Turn", target: str): + return await turn.player.make_enemy_discard(target) + + +async def effect_restack_discarded_champion(_effect: Effect, turn: "Turn", target: str): + return await turn.restack_discarded_champion(target) + + +async def effect_restack_discarded(_effect: Effect, turn: "Turn", target: str): + return await turn.player.put_on_stack_from_discard(target) + + +async def effect_stack_next_action(_effect: Effect, turn: "Turn", target: str): + return await turn.buy_card(target, turn.player.buy_on_stack) + + +async def effect_stack_next_card(_effect: Effect, turn: "Turn", target: str): + return await turn.buy_card(target, turn.stack_next_card_bought_check) + + +async def effect_play_next_card(_effect: Effect, turn: "Turn", target: str): + return await turn.buy_card(target, turn.player.buy_in_hand) + + +card_effect_all_behaviours = { + CardEffects.GOLD: effect_gold, + CardEffects.DAMAGE: effect_damage, + CardEffects.HEAL: effect_heal, + CardEffects.PREPARE: effect_prepare, + CardEffects.STUN: effect_stun, + CardEffects.SACRIFICE: effect_sacrifice, + CardEffects.DRAW: effect_draw, + CardEffects.DRAW_AND_DISCARD: effect_draw_and_discard, + CardEffects.DISCARD: effect_discard, + CardEffects.RESTACK_DISCARDED_CHAMPION: effect_restack_discarded_champion, + CardEffects.RESTACK_DISCARDED_CARD: effect_restack_discarded, + CardEffects.STACK_NEXT_ACTION_BOUGHT: effect_stack_next_action, + CardEffects.STACK_NEXT_CARD_BOUGHT: effect_stack_next_card, + CardEffects.PLAY_NEXT_CARD_BOUGHT: effect_play_next_card, +} + + +# Effect Times +def effect_times_same_faction(effect: Effect, turn: "Turn"): + times = 0 + for c in turn.player.board: + if c.faction == effect.card.faction and c in turn.cards_locked: + times += 1 + return times + + +def effect_times_other_same_faction(effect: Effect, turn: "Turn"): + times = 0 + for c in turn.player.board: + if ( + c.faction == effect.card.faction + and c != effect.card + and c in turn.cards_locked + ): + times += 1 + return times + + +def effect_times_champion(effect: Effect, turn: "Turn"): + times = 0 + for c in turn.player.board: + if c.card_type == CardType.CHAMPION and c in turn.cards_locked: + times += 1 + return times + + +def effect_times_other_champion(effect: Effect, turn: "Turn"): + times = 0 + for c in turn.player.board: + if ( + c.card_type == CardType.CHAMPION + and c != effect.card + and c in turn.cards_locked + ): + times += 1 + return times + + +def effect_times_other_guard(effect: Effect, turn: "Turn"): + times = 0 + for c in turn.player.board: + if ( + c.card_type == CardType.CHAMPION + and c.guard is True + and c != effect.card + and c in turn.cards_locked + ): + times += 1 + return times + + +effect_times_all_behaviours = { + EffectTimes.PER_CARD_OF_SAME_FACTION: effect_times_same_faction, + EffectTimes.PER_OTHER_CARD_OF_SAME_FACTION: effect_times_other_same_faction, + EffectTimes.PER_CHAMPION: effect_times_champion, + EffectTimes.PER_OTHER_CHAMPION: effect_times_other_champion, + EffectTimes.PER_OTHER_GUARD: effect_times_other_guard, +} + + +def create_effect_behaviour(effect: EffectParser) -> Awaitable[None]: + + async def effect_type_behaviour(_e: Effect, _t: "Turn"): + return True + + if effect.effect_type in effect_types_all_behaviours: + effect_type_behaviour = effect_types_all_behaviours[effect.effect_type] + + if effect.effect not in card_effect_all_behaviours: + raise NotImplementedError("CardEffect " + effect.effect + " not implemented") + + card_effect_behaviour = card_effect_all_behaviours[effect.effect] + + def effect_times_behaviour(_e: Effect, _t: "Turn"): + return 1 + + if effect.times in effect_times_all_behaviours: + effect_times_behaviour = effect_times_all_behaviours[effect.times] + + async def activate(self: Effect, turn: "Turn", target: Optional[str] = None): + effect_type_result = await effect_type_behaviour(self, turn) + if not effect_type_result: + return False + times = effect_times_behaviour(self, turn) + for _ in range(times): + effect_result = await card_effect_behaviour(self, turn, target) + if not effect_result: + return False + return True + + return activate diff --git a/src/model/card_loader.py b/src/model/card_loader.py index 4e389be..5be9b75 100644 --- a/src/model/card_loader.py +++ b/src/model/card_loader.py @@ -1,10 +1,12 @@ from json import JSONDecodeError +from typing import List, Dict, Optional from pathlib import Path from pydantic import BaseModel, ValidationError -from typing import List, Dict, Optional + from model.card_validator import CardParser, EffectParser from model.card import Effect, Card +from .card_behaviour import create_effect_behaviour class CardContainerFile(BaseModel): @@ -57,7 +59,7 @@ def create_card_effects( mutexes[mutex] = [] for effect in card.effects: - real_effect, sub_effects = create_effect(effect) + real_effect, sub_effects = create_effect_and_subeffects(effect) for e in sub_effects: all_effects.append(e) all_effects.append(real_effect) @@ -72,7 +74,7 @@ def create_card_effects( return combined, all_effects -def create_effect( +def create_effect_and_subeffects( effect: EffectParser, all_effects: Optional[List[Effect]] = None ) -> (Effect, List[Effect]): if all_effects is None: @@ -83,7 +85,7 @@ def create_effect( mutexes[mutex] = [] for s in effect.sub_effects: - real_effect, all_effects2 = create_effect(s, all_effects) + real_effect, all_effects2 = create_effect_and_subeffects(s, all_effects) for e in all_effects2: all_effects.append(e) all_effects.append(real_effect) @@ -95,13 +97,15 @@ def create_effect( if s.mutex in mutexes: sub_effects[index].mutex = mutexes[s.mutex] - return ( - Effect( - effect=effect.effect, - amount=effect.amount, - effect_type=effect.effect_type, - sub_effects=sub_effects, - times=effect.times, + actual_effect = Effect( + effect=effect.effect, + amount=effect.amount, + effect_type=effect.effect_type, + sub_effects=sub_effects, + times=effect.times, + activate=lambda *args, **kwargs: create_effect_behaviour(effect=effect)( + actual_effect, *args, **kwargs ), - all_effects, ) + + return actual_effect, all_effects diff --git a/src/model/turn.py b/src/model/turn.py index 5ac5553..a08c7fa 100644 --- a/src/model/turn.py +++ b/src/model/turn.py @@ -2,7 +2,7 @@ Checks the actions of the user and prevent the user to do something illegal """ -from typing import TYPE_CHECKING, List, Dict, Optional, Callable +from typing import TYPE_CHECKING, List, Dict, Optional, Callable, Coroutine, Any from uuid import UUID from model import ( Serializable, @@ -29,7 +29,7 @@ class Turn(Serializable): player: "Player" cards_locked: List[Card] effects_availables: List[Effect] - champions_health: Dict[UUID, int] + champions_health: Dict[str, int] discard_markers: int damage_reserve: int heal_reserve: int @@ -57,7 +57,13 @@ class Turn(Serializable): self.effects_availables = [] self.cards_locked = [] self.discard_markers = 0 - await self.player.game.notify_controller("update", self.serialize()) + for p in self.player.game.players: + p.turn.champions_health = {} + for c in p.board: + if c.card_type == CardType.CHAMPION: + p.turn.champions_health[str(c.uuid)] = c.defense + await p.game.notify_controller("update", p.turn.serialize()) + await self.player.end_turn() async def lock_card(self, card_id: str): @@ -70,38 +76,21 @@ class Turn(Serializable): return print("Card already locked") self.cards_locked.append(card) for effect in card.effects: - for _ in range(effect.amount): - match effect.times: - case EffectTimes.PER_CARD_OF_SAME_FACTION: - for c in self.player.board: - if c.faction == card.faction: - self.effects_availables.append(effect) - case EffectTimes.PER_OTHER_CARD_OF_SAME_FACTION: - for c in self.player.board: - if c.faction == card.faction and c != card: - self.effects_availables.append(effect) - case EffectTimes.PER_CHAMPION: - for c in self.player.board: - if c.card_role == CardType.CHAMPION: - self.effects_availables.append(effect) - case EffectTimes.PER_OTHER_CHAMPION: - for c in self.player.board: - if c.card_role == CardType.CHAMPION and c != card: - self.effects_availables.append(effect) - case EffectTimes.PER_OTHER_GUARD: - for c in self.player.board: - if ( - c.card_role == CardType.CHAMPION - and c.guard is True - and c != card - ): - self.effects_availables.append(effect) - case None: - self.effects_availables.append(effect) - case _: - raise NotImplementedError( - "EffectTimes " + effect.times + " not implemented" - ) + if effect.effect in [ + CardEffects.DAMAGE, + CardEffects.GOLD, + CardEffects.HEAL, + ]: + self.effects_availables.append(effect) + if ( + effect.effect_type is None + or effect.effect_type == EffectType.CHAMPION_ACTION + ): + if len(effect.mutex) == 0: + await self.use_effect_with_check(effect_id=str(effect.uuid)) + else: + for _ in range(effect.amount): + self.effects_availables.append(effect) await self.player.game.notify_controller("update", self.serialize()) @@ -121,7 +110,7 @@ class Turn(Serializable): return print("Cannot sacrifice : card is locked") return self.player.sacrifice_card(card_id) - async def use_effect_with_check(self, effect_id: str, target: Optional[str]): + async def use_effect_with_check(self, effect_id: str, target: Optional[str] = None): if self.discard_markers > 0: return print("You cannot use an effect while having discard markers") effect = next( @@ -130,47 +119,13 @@ class Turn(Serializable): ) if effect is None: return print("Effect not available") - - card = next( - (m for m in self.player.board if effect in m.effects), - None, - ) - if card is not None: - # TODO: Could break for subEffects - # Maybe store a reference to card in effect to fix this - # Also breaks for Sacrifice effects - - match effect.effect_type: - case EffectType.SUICIDE: - for e in card.effects: - if e != effect: - while e in self.effects_availables: - self.effects_availables.remove(e) - for se in e.sub_effects: - if se != effect: - while se in self.effects_availables: - self.effects_availables.remove(se) - await self.player.sacrifice_card(str(card.uuid)) - case EffectType.FACTION_COMBO: - if ( - len([c for c in self.player.board if c.faction == card.faction]) - <= 1 - ): - return print("effect cannot run : faction combo not reached") - case None: - pass - case EffectType.CHAMPION_ACTION: - pass - case _: - raise NotImplementedError( - "EffectType " + effect.effect_type + " not implemented" - ) - else: - print( + card = effect.card + if card is None: + return print( "Something went wrong while finding the card parent of an effect\nAre you using an effect from an already used card ?" ) - success = await self.use_effect(effect_id, target) + success = await effect.activate(self, target) if not success: return print("something went wrong while executing effect") @@ -186,59 +141,8 @@ class Turn(Serializable): for e in effect.sub_effects: for _ in range(e.amount): self.effects_availables.append(e) - print([str(e.uuid) for e in self.effects_availables]) await self.player.game.notify_controller("update", self.serialize()) - async def use_effect(self, effect_id: str, target: str): - effect = next( - (e for e in self.effects_availables if str(e.uuid) == effect_id), - None, - ) - - if effect is None: - print("effect couldn't found!") - return False - success = False - - match effect.effect: - case CardEffects.GOLD: - self.gold_reserve += 1 - success = True - case CardEffects.DAMAGE: - self.damage_reserve += 1 - success = True - case CardEffects.HEAL: - self.heal_reserve += 1 - success = True - case CardEffects.PERPARE: - success = self.prepare(target) - case CardEffects.STUN: - success = await self.player.stun(target) - case CardEffects.SACRIFICE: - success = await self.sacrifice_card(target) - case CardEffects.DRAW: - success = await self.player.draw(1) - case CardEffects.DRAW_AND_DISCARD: - self.discard_markers += 1 - success = await self.player.draw(1) - case CardEffects.DISCARD: - success = await self.player.make_enemy_discard(target) - case CardEffects.RESTACK_DISCARDED_CHAMPION: - success = await self.restack_discarded_champion(target) - case CardEffects.RESTACK_DISCARDED_CARD: - success = await self.player.put_on_stack_from_discard(target) - case CardEffects.STACK_NEXT_ACTION_BOUGHT: - success = await self.buy_card(target, self.player.buy_on_stack) - case CardEffects.STACK_NEXT_CARD_BOUGHT: - success = await self.buy_card(target, self.stack_next_card_bought_check) - case CardEffects.PLAY_NEXT_CARD_BOUGHT: - success = await self.buy_card(target, self.player.buy_in_hand) - case _: - raise NotImplementedError( - "CardEffect " + effect.effect + " not implemented" - ) - return success - def prepare(self, card_id: str) -> bool: card = next( (e for e in self.cards_locked if str(e.uuid) == card_id), @@ -256,12 +160,7 @@ class Turn(Serializable): return True async def start_turn(self) -> None: - self.champions_health = {} - for p in self.player.game.players: - for c in p.board: - if c.card_type == CardType.CHAMPION: - p.turn.champions_health[str(c.uuid)] = c.defense - await p.game.notify_controller("update", p.turn.serialize()) + pass def serialize(self): data = super().serialize() | { @@ -291,7 +190,9 @@ class Turn(Serializable): return await self.player.buy_on_stack(card) async def buy_card( - self, card_id: str, buy_function: Optional[Callable[[Card], bool]] = None + self, + card_id: str, + buy_function: Optional[Callable[[Card], Coroutine[Any, Any, bool]]] = None, ): if buy_function is None: buy_function = self.player.buy_card @@ -331,7 +232,7 @@ class Turn(Serializable): if p.team != team: continue for c in p.board: - if c.guard == True: + if c.guard: return True return False