fixes #1; Migrate card activation effect to factory in card_behaviour.py

This commit is contained in:
2024-05-02 23:10:04 +02:00
parent 2e04462d86
commit 191d32bea2
4 changed files with 275 additions and 148 deletions
+11 -3
View File
@@ -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 import Serializable, CardRole, CardType, CardFaction
from model.card_validator import CardEffects, EffectType, EffectTimes from model.card_validator import CardEffects, EffectType, EffectTimes
if TYPE_CHECKING:
from model import Player, Turn
class Effect(Serializable): class Effect(Serializable):
"""Represents an effect of a card (gold, damage, heal, draw a card...)""" """Represents an effect of a card (gold, damage, heal, draw a card...)"""
@@ -13,12 +16,14 @@ class Effect(Serializable):
mutex: List["Effect"] mutex: List["Effect"]
effect_type: Optional[EffectType] = None effect_type: Optional[EffectType] = None
times: Optional[EffectTimes] = None times: Optional[EffectTimes] = None
card: Optional["Card"] = None card: "Card"
activate: Callable[["Turn"], Union["Player", "Card"]]
def __init__( def __init__(
self, self,
effect: CardEffects, effect: CardEffects,
amount: int, amount: int,
activate: Callable[["Turn"], Union["Player", "Card"]],
sub_effects: Optional[List["Effect"]] = None, sub_effects: Optional[List["Effect"]] = None,
effect_type: Optional[EffectType] = None, effect_type: Optional[EffectType] = None,
times: Optional[EffectTimes] = None, times: Optional[EffectTimes] = None,
@@ -31,18 +36,21 @@ class Effect(Serializable):
sub_effects = [] sub_effects = []
self.effect = effect self.effect = effect
self.amount = amount self.amount = amount
self.activate = activate
self.sub_effects = sub_effects self.sub_effects = sub_effects
self.effect_type = effect_type self.effect_type = effect_type
self.mutex = mutex self.mutex = mutex
self.times = times self.times = times
def serialize(self) -> dict: def serialize(self) -> dict:
return super().serialize() | { data = super().serialize() | {
"sub_effects": [effect.uuid for effect in self.sub_effects], "sub_effects": [effect.uuid for effect in self.sub_effects],
"mutex": [effect.uuid for effect in self.mutex], "mutex": [effect.uuid for effect in self.mutex],
"card": self.card.uuid, "card": self.card.uuid,
} }
return {k: data[k] for k in data.keys() - ["activate"]}
class Card(Serializable): class Card(Serializable):
"""Represents any playable card""" """Represents any playable card"""
+214
View File
@@ -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
+16 -12
View File
@@ -1,10 +1,12 @@
from json import JSONDecodeError from json import JSONDecodeError
from typing import List, Dict, Optional
from pathlib import Path from pathlib import Path
from pydantic import BaseModel, ValidationError from pydantic import BaseModel, ValidationError
from typing import List, Dict, Optional
from model.card_validator import CardParser, EffectParser from model.card_validator import CardParser, EffectParser
from model.card import Effect, Card from model.card import Effect, Card
from .card_behaviour import create_effect_behaviour
class CardContainerFile(BaseModel): class CardContainerFile(BaseModel):
@@ -57,7 +59,7 @@ def create_card_effects(
mutexes[mutex] = [] mutexes[mutex] = []
for effect in card.effects: 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: for e in sub_effects:
all_effects.append(e) all_effects.append(e)
all_effects.append(real_effect) all_effects.append(real_effect)
@@ -72,7 +74,7 @@ def create_card_effects(
return combined, all_effects return combined, all_effects
def create_effect( def create_effect_and_subeffects(
effect: EffectParser, all_effects: Optional[List[Effect]] = None effect: EffectParser, all_effects: Optional[List[Effect]] = None
) -> (Effect, List[Effect]): ) -> (Effect, List[Effect]):
if all_effects is None: if all_effects is None:
@@ -83,7 +85,7 @@ def create_effect(
mutexes[mutex] = [] mutexes[mutex] = []
for s in effect.sub_effects: 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: for e in all_effects2:
all_effects.append(e) all_effects.append(e)
all_effects.append(real_effect) all_effects.append(real_effect)
@@ -95,13 +97,15 @@ def create_effect(
if s.mutex in mutexes: if s.mutex in mutexes:
sub_effects[index].mutex = mutexes[s.mutex] sub_effects[index].mutex = mutexes[s.mutex]
return ( actual_effect = Effect(
Effect( effect=effect.effect,
effect=effect.effect, amount=effect.amount,
amount=effect.amount, effect_type=effect.effect_type,
effect_type=effect.effect_type, sub_effects=sub_effects,
sub_effects=sub_effects, times=effect.times,
times=effect.times, activate=lambda *args, **kwargs: create_effect_behaviour(effect=effect)(
actual_effect, *args, **kwargs
), ),
all_effects,
) )
return actual_effect, all_effects
+34 -133
View File
@@ -2,7 +2,7 @@
Checks the actions of the user and prevent the user to do something illegal 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 uuid import UUID
from model import ( from model import (
Serializable, Serializable,
@@ -29,7 +29,7 @@ class Turn(Serializable):
player: "Player" player: "Player"
cards_locked: List[Card] cards_locked: List[Card]
effects_availables: List[Effect] effects_availables: List[Effect]
champions_health: Dict[UUID, int] champions_health: Dict[str, int]
discard_markers: int discard_markers: int
damage_reserve: int damage_reserve: int
heal_reserve: int heal_reserve: int
@@ -57,7 +57,13 @@ class Turn(Serializable):
self.effects_availables = [] self.effects_availables = []
self.cards_locked = [] self.cards_locked = []
self.discard_markers = 0 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() await self.player.end_turn()
async def lock_card(self, card_id: str): async def lock_card(self, card_id: str):
@@ -70,38 +76,21 @@ class Turn(Serializable):
return print("Card already locked") return print("Card already locked")
self.cards_locked.append(card) self.cards_locked.append(card)
for effect in card.effects: for effect in card.effects:
for _ in range(effect.amount): if effect.effect in [
match effect.times: CardEffects.DAMAGE,
case EffectTimes.PER_CARD_OF_SAME_FACTION: CardEffects.GOLD,
for c in self.player.board: CardEffects.HEAL,
if c.faction == card.faction: ]:
self.effects_availables.append(effect) self.effects_availables.append(effect)
case EffectTimes.PER_OTHER_CARD_OF_SAME_FACTION: if (
for c in self.player.board: effect.effect_type is None
if c.faction == card.faction and c != card: or effect.effect_type == EffectType.CHAMPION_ACTION
self.effects_availables.append(effect) ):
case EffectTimes.PER_CHAMPION: if len(effect.mutex) == 0:
for c in self.player.board: await self.use_effect_with_check(effect_id=str(effect.uuid))
if c.card_role == CardType.CHAMPION: else:
self.effects_availables.append(effect) for _ in range(effect.amount):
case EffectTimes.PER_OTHER_CHAMPION: self.effects_availables.append(effect)
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"
)
await self.player.game.notify_controller("update", self.serialize()) await self.player.game.notify_controller("update", self.serialize())
@@ -121,7 +110,7 @@ class Turn(Serializable):
return print("Cannot sacrifice : card is locked") return print("Cannot sacrifice : card is locked")
return self.player.sacrifice_card(card_id) 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: if self.discard_markers > 0:
return print("You cannot use an effect while having discard markers") return print("You cannot use an effect while having discard markers")
effect = next( effect = next(
@@ -130,47 +119,13 @@ class Turn(Serializable):
) )
if effect is None: if effect is None:
return print("Effect not available") return print("Effect not available")
card = effect.card
card = next( if card is None:
(m for m in self.player.board if effect in m.effects), return print(
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(
"Something went wrong while finding the card parent of an effect\nAre you using an effect from an already used card ?" "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: if not success:
return print("something went wrong while executing effect") return print("something went wrong while executing effect")
@@ -186,59 +141,8 @@ class Turn(Serializable):
for e in effect.sub_effects: for e in effect.sub_effects:
for _ in range(e.amount): for _ in range(e.amount):
self.effects_availables.append(e) self.effects_availables.append(e)
print([str(e.uuid) for e in self.effects_availables])
await self.player.game.notify_controller("update", self.serialize()) 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: def prepare(self, card_id: str) -> bool:
card = next( card = next(
(e for e in self.cards_locked if str(e.uuid) == card_id), (e for e in self.cards_locked if str(e.uuid) == card_id),
@@ -256,12 +160,7 @@ class Turn(Serializable):
return True return True
async def start_turn(self) -> None: async def start_turn(self) -> None:
self.champions_health = {} pass
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())
def serialize(self): def serialize(self):
data = super().serialize() | { data = super().serialize() | {
@@ -291,7 +190,9 @@ class Turn(Serializable):
return await self.player.buy_on_stack(card) return await self.player.buy_on_stack(card)
async def buy_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: if buy_function is None:
buy_function = self.player.buy_card buy_function = self.player.buy_card
@@ -331,7 +232,7 @@ class Turn(Serializable):
if p.team != team: if p.team != team:
continue continue
for c in p.board: for c in p.board:
if c.guard == True: if c.guard:
return True return True
return False return False