Coverage for thb/actions.py : 92%
Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# -*- coding: utf-8 -*-
2from __future__ import annotations
4# -- stdlib --
5from collections import defaultdict
6from copy import copy
7from dataclasses import dataclass
8from typing import Any, Dict, List, Optional, Sequence, Set, TYPE_CHECKING, Tuple, Type, Union, cast
9import logging
11# -- third party --
12from typing_extensions import Literal, Protocol
14# -- own --
15from game.base import Action, ActionShootdown, EventArbiter, GameViralContext, InputTransaction
16from game.base import Player, sync_primitive
17from thb.cards.base import Card, CardList, PhysicalCard, Skill, VirtualCard
18from thb.common import PlayerRole
19from thb.inputlets import ActionInputlet, ChoosePeerCardInputlet
20from thb.mode import THBAction, THBEventHandler, THBPlayerAction, THBattle
21from utils.check import CheckFailed, check, check_type
22from utils.misc import BatchList
24# -- typing --
25if TYPE_CHECKING:
26 from thb.characters.base import Character # noqa: F401
29# -- code --
30log = logging.getLogger('THBattle_Actions')
33# ------------------------------------------
34# aux functions
36def ttags(actor):
37 tags = actor.tags
38 tc = tags['turn_count']
39 return tags.setdefault('turn_tags:%s' % tc, defaultdict(int))
42class CardChooser(Protocol):
43 game: THBattle
44 card_usage: str
46 def cond(self, cards: Sequence[Card]) -> bool: ... 46 ↛ exitline 46 didn't return from function 'cond'
49class CharacterChooser(Protocol):
50 game: THBattle
52 def choose_player_target(self, pl: Sequence[Character]) -> Tuple[List[Character], bool]: ... 52 ↛ exitline 52 didn't return from function 'choose_player_target'
55def ask_for_action(initiator: Union[CardChooser, CharacterChooser],
56 actors: List[Character],
57 categories: Sequence[str],
58 candidates: Sequence[Character],
59 timeout: Optional[int] = None,
60 trans: Optional[InputTransaction] = None,
61 ) -> Tuple[Optional[Character], Optional[Tuple[List[Card], List[Character]]]]:
62 # initiator: Action or EH requesting this
63 # actors: players involved
64 # categories: card categories, eg: ['cards', 'showncards']
65 # candidates: players can be selection target, eg: g.players
67 assert categories or candidates
68 assert actors
70 timeout = timeout or 25
72 from thb.cards.base import VirtualCard
74 g = cast(THBattle, initiator.game)
76 ilet = ActionInputlet(initiator, categories, candidates)
78 @ilet.with_post_process
79 def process(actor: Character, rst):
80 usage = getattr(initiator, 'card_usage', 'none')
81 try:
82 check(rst)
84 skills: List[Type[Skill]]
85 rawcards: List[Card]
86 characters: List[Character]
87 params: Dict[str, Any]
89 skills, rawcards, characters, params = rst
90 [check(not c.detached) for c in rawcards]
91 [check(actor.has_skill(s)) for s in skills] # has_skill may be hooked
93 if skills:
94 cards = [skill_wrap(actor, skills, rawcards, params)]
95 usage = cards[0].usage if usage == 'launch' else usage
96 assert usage != 'none', (cards[0], cards[0].usage)
97 else:
98 cards = rawcards
99 usage = 'launch'
101 if categories:
102 if len(cards) == 1 and cards[0].is_card(VirtualCard):
103 def walk(c: Card):
104 if not c.is_card(VirtualCard): return
105 if getattr(c, 'no_reveal', False): return
107 c = cast(VirtualCard, c)
109 g.players.player.reveal(c.associated_cards)
110 for c1 in c.associated_cards:
111 walk(c1)
113 walk(cards[0])
114 check(skill_check(cards[0]))
116 else:
117 if not getattr(initiator, 'no_reveal', False):
118 g.players.player.reveal(cards)
120 check(cast(CardChooser, initiator).cond(cards))
121 assert not (usage == 'none' and rawcards), (skills, rawcards, characters, params) # should not pass check
122 else:
123 cards = []
125 if candidates:
126 characters, valid = cast(CharacterChooser, initiator).choose_player_target(characters)
127 check(valid)
129 ask_for_action_verify = getattr(initiator, 'ask_for_action_verify', None)
131 if ask_for_action_verify:
132 check(ask_for_action_verify(actor, cards, characters))
134 return cards, characters, params
136 except CheckFailed:
137 return None
139 p, rst = g.user_input(actors, ilet, timeout=timeout, type='any', trans=trans)
140 if rst:
141 cards, characters, params = rst
143 if len(cards) == 1 and cards[0].is_card(VirtualCard):
144 g.deck.register_vcard(cards[0])
146 if not cards and not characters: 146 ↛ 147line 146 didn't jump to line 147, because the condition on line 146 was never true
147 return p, None
149 [c.detach() for c in VirtualCard.unwrap(cards)]
151 return p, (cards, characters)
152 else:
153 return None, None
156def user_choose_cards(initiator: CardChooser,
157 actor: Character,
158 categories: Sequence[str],
159 timeout: Optional[int] = None,
160 trans: Optional[InputTransaction] = None,
161 ) -> Optional[List[Card]]:
162 check_type([str, ...], categories)
164 _, rst = ask_for_action(initiator, [actor], categories, (), timeout=timeout, trans=trans)
165 if not rst:
166 return None
168 return rst[0] # cards
171def user_choose_players(initiator: CharacterChooser,
172 actor: Character,
173 candidates: List[Character],
174 timeout: Optional[int] = None,
175 trans: Optional[InputTransaction] = None,
176 ) -> Optional[List[Character]]:
177 _, rst = ask_for_action(initiator, [actor], (), candidates, timeout=timeout, trans=trans)
178 if not rst:
179 return None
181 return rst[1] # players
184def random_choose_card(g: THBattle, cardlists: Sequence[Sequence]):
185 from itertools import chain
186 allcards = list(chain.from_iterable(cardlists))
187 if not allcards:
188 return None
190 c = g.random.choice(allcards)
191 v = sync_primitive(c.sync_id, g.players.player)
192 c = g.deck.lookup(v)
193 assert c
194 c.detach()
195 return c
198def skill_wrap(actor: Character, skills: List[Type[Skill]], cards: List[Card], params: Dict[str, Any]):
199 assert skills
200 for skill_cls in skills:
201 card = skill_cls.wrap(cards, actor, params)
202 cards = [card]
204 return cards[0]
207def skill_check(wrapped):
208 from thb.cards.base import Skill
210 try:
211 check(wrapped.check())
212 for c in wrapped.associated_cards:
213 if c.is_card(Skill): 213 ↛ 214line 213 didn't jump to line 214, because the condition on line 213 was never true
214 check(c.character is wrapped.character)
215 check(skill_check(c))
216 else:
217 check(c.resides_in.owner is wrapped.character)
219 return True
221 except CheckFailed:
222 return False
225COMMON_EVENT_HANDLERS: Set[Type[THBEventHandler]] = set()
228def register_eh(cls):
229 COMMON_EVENT_HANDLERS.add(cls)
230 return cls
233# ------------------------------------------
234class GenericAction(THBAction):
235 pass
238class UserAction(THBAction): # card/character skill actions
239 target_list: Sequence[Character]
240 associated_card: Card
243@dataclass
244class CardMigration:
245 trans: MigrateCardsTransaction
246 cards: List[Card]
247 to: CardList
248 unwrap: bool = True # will cards be unwrapped?
249 direction: Literal['front', 'back'] = 'front'
252@dataclass
253class CardMovement:
254 trans: MigrateCardsTransaction
255 card: Card
256 fr: CardList
257 to: CardList
258 direction: Literal['front', 'back'] = 'front'
261class MigrateCardsTransaction(GameViralContext):
262 movements: List[CardMovement]
264 def __init__(self, action: Optional[THBAction] = None):
265 g = self.game
266 self.action = cast(THBAction, action or g.action_stack[-1])
268 if self.action is g.action_stack[-1]:
269 # Ensure no card movements in EventHandlers!
270 assert self.action is g.hybrid_stack[-1], (self.action, g.hybrid_stack[-1])
272 self.cancelled = False
273 self.migrations = []
274 self.movements = []
276 def __repr__(self):
277 return 'MCT'
279 def add_step(self, m: CardMigration) -> None:
280 self.migrations.append(m)
282 def __enter__(self):
283 return self
285 def __exit__(self, *excinfo):
286 if not self.cancelled:
287 self.commit()
288 else:
289 log.debug('migrate_cards cancelled: %s', self.movements)
291 def commit(self):
292 g = self.game
294 seen = set()
296 for m in self.migrations:
297 cards = VirtualCard.unwrap(m.cards, include_vcards=True)
298 n = 0
299 for c in cards:
300 if c in seen:
301 continue
303 if isinstance(c, VirtualCard):
304 if not m.unwrap:
305 assert m.to.owner, 'Invalid move!'
306 sp = m.to.owner.special
307 for ac in c.associated_cards:
308 assert ac not in seen
309 if not isinstance(ac, VirtualCard): 309 ↛ 307line 309 didn't jump to line 307, because the condition on line 309 was never false
310 self.movements.append(CardMovement(
311 trans=self, card=ac, fr=ac.resides_in, to=sp, direction='front'
312 ))
313 ac.move_to(sp)
314 seen.add(ac)
315 else:
316 c.detach()
317 c.unwrapped = True
318 continue
320 self.movements.append(CardMovement(
321 trans=self, card=c, fr=c.resides_in, to=m.to, direction=m.direction
322 ))
323 c.move_to(m.to)
324 seen.add(c)
325 n += 1
327 if m.direction == 'back':
328 m.to.rotate(n)
330 g.emit_event('post_card_migration', self)
333def migrate_cards(cards: Sequence[Card],
334 to: CardList,
335 unwrap: bool = False,
336 direction: Literal['front', 'back'] = 'front',
337 trans: Optional[MigrateCardsTransaction] = None,
338 ):
339 '''
340 cards: cards to move around
341 to: destination card list
342 unwrap: tear down VirtualCard wrapping, preserve PhysicalCard only
343 trans: associated MigrateCardsTransaction
344 '''
345 if not trans:
346 with MigrateCardsTransaction() as t:
347 migrate_cards(cards, to, unwrap, direction, t)
348 return not t.cancelled
350 assert to is not None
352 if to.owner and to.owner.dead:
353 # do not migrate cards to dead character
354 trans.cancelled = True
355 return
357 trans.add_step(CardMigration(
358 trans=trans,
359 cards=cards,
360 to=to,
361 unwrap=unwrap,
362 direction=direction,
363 ))
366class PostCardMigrationHandler(EventArbiter):
367 interested = ['post_card_migration']
369 def handle(self, evt_type, arg):
370 if evt_type != 'post_card_migration': return arg 370 ↛ exitline 370 didn't return from function 'handle', because the return on line 370 wasn't executed
372 g = self.game
373 act = arg.action
374 tgt = act.target or act.source or g.players[0]
376 n = len(g.players)
377 try:
378 idx = g.players.index(tgt) - n
379 except ValueError:
380 # Character died or switched out or whatever
381 return arg
382 for i in range(idx, idx + n):
383 for eh in self.handlers:
384 g.dispatcher.handle_single_event(eh, g.players[i], arg)
386 return arg
389def detach_cards(cards: Sequence[Card], trans=None):
390 g = MigrateCardsTransaction().game
391 for c in VirtualCard.unwrap(cards):
392 c.detach()
393 g.emit_event('detach_cards', cards)
396class DeadDropCards(GenericAction):
397 def apply_action(self):
398 tgt = self.target
399 g = self.game
401 others = g.players.exclude(tgt)
402 lists = [tgt.cards, tgt.showncards, tgt.equips, tgt.fatetell, tgt.special]
403 lists.extend(tgt.showncardlists)
404 for cl in lists:
405 if not cl: continue
406 others.reveal(list(cl))
407 g.process_action(DropCards(tgt, tgt, cl))
408 assert not cl
410 return True
413class PlayerDeath(GenericAction):
414 # FIXME: should be `CharacterDeath`
415 def apply_action(self) -> bool:
416 tgt = self.target
417 tgt.dead = True
418 tgt.skills[:] = []
419 tgt.tags.clear()
420 g = self.game
421 g.process_action(DeadDropCards(tgt, tgt))
422 return True
425class PlayerRevive(GenericAction):
426 def __init__(self, source, target, hp):
427 self.source = source
428 self.target = target
429 self.hp = hp
431 def apply_action(self):
432 tgt = self.target
433 assert tgt.dead
435 tgt.dead = False
436 tgt.maxlife = tgt.__class__.maxlife
437 tgt.skills = list(tgt.__class__.skills)
439 tgt.life = min(tgt.maxlife, self.hp)
440 return True
442 def is_valid(self):
443 return self.target.dead
446class TryRevive(GenericAction):
447 def __init__(self, target: Character, dmgact: BaseDamage):
448 self.source = self.target = target
449 self.dmgact = dmgact
450 self.revived_by: Optional[Character] = None
451 if target.dead: 451 ↛ 452line 451 didn't jump to line 452, because the condition on line 451 was never true
452 log.error('TryRevive buggy condition, __init__')
453 return
455 def apply_action(self) -> bool:
456 tgt = self.target
458 if tgt.dead: 458 ↛ 459line 458 didn't jump to line 459, because the condition on line 458 was never true
459 log.error('TryRevive buggy condition, apply')
460 import traceback
461 traceback.print_stack()
462 return False
464 g = self.game
465 from thb.cards.basic import AskForHeal
466 for p in g.players.rotate_to(tgt):
467 while True:
468 if p.dead:
469 break
471 if g.process_action(AskForHeal(tgt, p)):
472 if tgt.life > 0: 472 ↛ 475line 472 didn't jump to line 475, because the condition on line 472 was never false
473 self.revived_by = p
474 return True
475 continue
477 break
479 return tgt.life > 0
481 def is_valid(self):
482 tgt = self.target
483 return not tgt.dead and tgt.maxlife > 0
486class BaseDamage(GenericAction):
487 def __init__(self, source, target, amount=1):
488 self.source = source
489 self.target = target
490 self.amount = amount
492 def apply_action(self):
493 tgt = self.target
494 tgt.life -= self.amount
495 return True
497 def is_valid(self):
498 return not self.target.dead
501class Damage(BaseDamage):
502 pass
505class LifeLost(BaseDamage):
506 def __init__(self, source, target, amount=1):
507 self.source = None
508 self.target = target
509 self.amount = amount
512class MaxLifeChange(GenericAction):
513 def __init__(self, source, target, amount):
514 self.source = source
515 self.target = target
516 self.amount = amount
518 def apply_action(self):
519 src = self.source
520 tgt = self.target
521 g = self.game
522 tgt.maxlife += self.amount
524 if tgt.life > tgt.maxlife:
525 g.process_action(
526 LifeLost(src, tgt, abs(tgt.life - tgt.maxlife))
527 )
528 assert tgt.life == tgt.maxlife
530 assert tgt.maxlife or tgt.dead
532 return True
534 def is_valid(self):
535 return not self.target.dead
538# ---------------------------------------------------
540class DropCards(GenericAction):
541 def __init__(self, source, target, cards):
542 self.source = source
543 self.target = target
544 self.cards = cards
546 def apply_action(self):
547 g = self.game
548 target = self.target
549 cards = self.cards
550 assert all(c.resides_in.owner in (target, None) for c in cards), 'WTF?!'
551 migrate_cards(cards, g.deck.droppedcards, unwrap=True)
553 return True
555 def is_valid(self):
556 return True
559class UseCard(GenericAction):
561 def __init__(self, target, card):
562 self.source = self.target = target
563 self.card = card
565 def apply_action(self):
566 g = self.game
567 tgt = self.target
568 c = self.card
569 act = getattr(c, 'use_action', None)
570 if act:
571 return g.process_action(act(tgt, c))
572 else:
573 migrate_cards([c], g.deck.droppedcards, unwrap=True)
574 return True
576 def can_fire(self):
577 c = self.card
578 act = getattr(c, 'use_action', None)
579 if act:
580 return act(self.target, self.card).can_fire()
581 else:
582 return True
585class AskForCard(GenericAction):
587 def __init__(self, source: Character, target: Character, card_cls: Type[PhysicalCard], categories: Sequence[str] = ('cards', 'showncards')):
588 self.source = source
589 self.target = target
590 self.card_cls = card_cls
591 self.categories = categories
593 self.card: Optional[Card] = None
595 def apply_action(self):
596 target = self.target
598 if not self.card: # ask if not already provided
599 cards = user_choose_cards(self, target, self.categories)
600 if not cards or len(cards) != 1:
601 self.card = None
602 return False
604 self.card = cards[0]
606 return self.process_card(self.card)
608 def cond(self, cl):
609 from thb.cards.base import VirtualCard
610 t = self.target
611 return (
612 len(cl) == 1 and
613 cl[0].is_card(self.card_cls) and
614 (cl[0].is_card(VirtualCard) or cl[0].resides_in.owner is t)
615 )
617 def process_card(self, card):
618 raise NotImplementedError
621class ActiveDropCards(GenericAction):
622 card_usage = 'drop'
624 def __init__(self, source: Character, target: Character, dropn: int) -> None:
625 self.source = source
626 self.target = target
627 self.dropn = dropn
628 self.cards: List[Card] = []
630 def apply_action(self) -> bool:
631 tgt = self.target
632 if tgt.dead: return False
633 n = self.dropn
634 if n <= 0: return True
636 g = self.game
637 cards = user_choose_cards(self, tgt, ('cards', 'showncards'))
638 if cards:
639 g.process_action(DropCards(tgt, tgt, cards=cards))
640 else:
641 from itertools import chain
642 cards = list(chain(tgt.cards, tgt.showncards))[min(-n, 0):]
643 g.players.player.reveal(cards)
644 g.process_action(DropCards(tgt, tgt, cards=cards))
646 self.cards = cards
647 return True
649 def cond(self, cards: Sequence[Card]) -> bool:
650 tgt = self.target
651 if not len(cards) == self.dropn:
652 return False
654 if not all(c.resides_in in (tgt.cards, tgt.showncards) for c in cards):
655 return False
657 from thb.cards.base import Skill
658 if any(c.is_card(Skill) for c in cards):
659 return False
661 return True
664class DropCardStage(ActiveDropCards):
665 def __init__(self, target):
666 dropn = len(target.cards) + len(target.showncards) - target.life
667 ActiveDropCards.__init__(self, target, target, dropn)
670class BaseDrawCards(GenericAction):
671 def __init__(self, target, amount=2, back=False):
672 self.source = self.target = target
673 self.amount = amount
674 self.back = back
676 def apply_action(self):
677 g = self.game
678 target = self.target
680 if self.back: 680 ↛ 681line 680 didn't jump to line 681, because the condition on line 680 was never true
681 g.deck.getcards(self.amount) # forcing dropped cards to join if cards in deck are insufficient
682 g.deck.cards.rotate(self.amount)
684 cards = g.deck.getcards(self.amount)
686 target.reveal(cards)
687 migrate_cards(cards, target.cards)
688 self.cards = cards
689 return True
691 def is_valid(self):
692 return not self.target.dead
695class DrawCards(BaseDrawCards):
696 pass
699class DistributeCards(BaseDrawCards):
700 pass
703class DrawCardStage(DrawCards):
704 pass
707class PutBack(GenericAction):
708 def __init__(self, source, cards, front=True):
709 self.source = self.target = source
710 self.cards = cards
711 self.front = front
713 def apply_action(self):
714 g = self.game
715 cards = self.cards
717 migrate_cards(cards, g.deck.cards, front=self.front)
719 return True
722class LaunchCard(GenericAction):
723 def __init__(self, src: Character,
724 target_list: Sequence[Character],
725 card: Card,
726 action: Optional[UserAction] = None,
727 bypass_check=False):
728 self.force_action = action
729 bypass_check = bool(action) or bypass_check
730 self.bypass_check = bypass_check
731 if bypass_check:
732 tl, tl_valid = target_list, True
733 else:
734 tl, tl_valid = card.target(src, target_list)
736 self.source, self.target_list, self.card, self.tl_valid = src, tl, card, tl_valid
737 self.target = target_list[0] if target_list else src
739 def apply_action(self) -> bool:
740 card = self.card
741 target_list = self.target_list
742 if not card: return False 742 ↛ exitline 742 didn't return from function 'apply_action', because the return on line 742 wasn't executed
744 if not self.force_action and not card.associated_action: 744 ↛ 745line 744 didn't jump to line 745, because the condition on line 744 was never true
745 return False
747 g = self.game
748 src = self.source
749 drop = card.usage == 'drop'
750 try:
751 if drop: # should drop before action 751 ↛ 752line 751 didn't jump to line 752, because the condition on line 751 was never true
752 g.process_action(DropCards(src, src, cards=[card]))
754 elif not getattr(card, 'no_drop', False):
755 detach_cards([card]) # emit events
757 else:
758 card.detach()
760 _, tl = g.emit_event('choose_target', (self, target_list))
761 assert _ is self
762 card = self.card # card may be touched
764 if not tl:
765 return True
767 if self.force_action:
768 a = self.force_action
769 else:
770 tgt = tl[0] if tl else src
771 assert card.associated_action
772 a = card.associated_action(source=src, target=tgt)
773 a.target_list = tl
775 a.associated_card = card
776 self.card_action = a
778 _ = g.emit_event('post_choose_target', (self, tl))
779 card = self.card # card may be touched
780 assert _ == (self, tl)
782 g.process_action(a)
783 card = self.card # card may be touched
785 return True
786 finally:
787 # card may be touched
788 # e.g. Kyouko Echo will move this card, left a placeholder here.
789 card = self.card
791 if not drop and card.detached:
792 # card/skill still in disputed state,
793 # means no actions have done anything to the card/skill,
794 # drop it
795 if not getattr(card, 'no_drop', False) and not card.unwrapped:
796 migrate_cards([card], g.deck.droppedcards, unwrap=True)
798 else:
799 from thb.cards.base import VirtualCard
800 for c in VirtualCard.unwrap([card]):
801 if c.detached: c.attach()
803 return True
805 def is_valid(self) -> bool:
806 if self.bypass_check:
807 return True
809 if not self.tl_valid:
810 log.debug('LaunchCard.tl_valid FALSE')
811 return False
813 card = self.card
814 if not card: 814 ↛ 815line 814 didn't jump to line 815, because the condition on line 814 was never true
815 log.debug('LaunchCard.card FALSE')
816 return False
818 g = self.game
819 src = self.source
821 dist = self.calc_distance(g, src, card)
822 if not all([dist[p] <= 0 for p in self.target_list]):
823 log.debug('LaunchCard: does not fulfill distance constraint')
824 return False
826 cls = card.associated_action
827 if not cls: 827 ↛ 828line 827 didn't jump to line 828, because the condition on line 827 was never true
828 return False
830 tl = self.target_list
831 target = tl[0] if tl else src
832 act = cls(source=src, target=target)
833 act.associated_card = card
834 act.target_list = tl
835 try:
836 act.action_shootdown_exception()
837 except Exception:
838 log.debug('LaunchCard card_action.can_fire() FALSE')
839 raise
841 return True
843 @classmethod
844 def calc_distance(cls, g, src, card):
845 dist = cls.calc_raw_distance(g, src, card)
846 card_dist = getattr(card, 'distance', 1000)
847 for p in dist:
848 dist[p] -= card_dist
849 g.emit_event('post_calcdistance', (src, card, dist))
850 return dist
852 @classmethod
853 def calc_raw_distance(cls, g, src, card):
854 dist = cls.calc_base_distance(g, src)
855 g.emit_event('calcdistance', (src, card, dist))
856 return dist
858 @classmethod
859 def calc_base_distance(cls, g, src):
860 pl = [p for p in g.players if not p.dead or p is src]
861 loc = pl.index(src)
862 n = len(pl)
863 dist = {
864 p: min(abs(i), n - abs(i))
865 for p, i in zip(pl, range(-loc, -loc + n))
866 }
867 return dist
870class ActionStageLaunchCard(LaunchCard):
871 pass
874class BaseActionStage(GenericAction):
875 card_usage = 'launch'
876 launch_card_cls: Type[LaunchCard]
878 def __init__(self, target):
879 self.source = self.source = target
880 self.target = target
881 self.in_user_input = False
882 self._force_break = False
883 self.action_count = 0
885 def apply_action(self):
886 g = self.game
887 target = self.target
888 if target.dead: return False
890 try:
891 while not target.dead:
892 try:
893 g.emit_event('action_stage_action', target)
894 self.in_user_input = True
895 with InputTransaction('ActionStageAction', [target]) as trans:
896 p, rst = ask_for_action(
897 self, [target], ('cards', 'showncards'), g.players, trans=trans
898 )
899 check(p is target)
900 assert rst
901 finally:
902 self.in_user_input = False
904 cards, target_list = rst
905 g.players.reveal(cards)
906 card = cards[0]
908 lc = self.launch_card_cls(target, target_list, card)
909 if not g.process_action(lc):
910 if lc.invalid: 910 ↛ 911line 910 didn't jump to line 911, because the condition on line 910 was never true
911 log.debug('ActionStage: LaunchCard invalid.')
912 check(False)
914 self.action_count += 1
916 if self.one_shot or self._force_break:
917 break
919 except CheckFailed:
920 pass
922 return True
924 @staticmethod
925 def force_break(g):
926 for a in g.action_stack: 926 ↛ exitline 926 didn't return from function 'force_break', because the loop on line 926 didn't complete
927 if isinstance(a, ActionStage):
928 a._force_break = True
929 log.debug('ActioStage: force_break requested')
930 break
932 def cond(self, cl):
933 if not cl: return False
935 tgt = self.target
936 if not len(cl) == 1: 936 ↛ 937line 936 didn't jump to line 937, because the condition on line 936 was never true
937 return False
939 c = cl[0]
940 return (
941 c.is_card(Skill) or c.resides_in in (tgt.cards, tgt.showncards)
942 ) and bool(c.associated_action)
944 def ask_for_action_verify(self, p, cl, tl):
945 assert len(cl) == 1
946 return self.launch_card_cls(p, tl, cl[0]).can_fire()
948 def choose_player_target(self, tl):
949 return tl, True
952class ActionStage(BaseActionStage):
953 one_shot = False
954 launch_card_cls = ActionStageLaunchCard
957@register_eh
958class ShuffleHandler(THBEventHandler):
959 interested = ['action_after', 'action_before', 'action_stage_action', 'user_input_start']
961 def handle(self, evt_type, arg):
962 g = self.game
963 if evt_type == 'action_stage_action':
964 self.do_shuffle(g, g.players)
966 elif evt_type in ('action_before', 'action_after') and isinstance(arg, ActionStage):
967 self.do_shuffle(g, g.players)
969 elif evt_type == 'user_input_start':
970 trans, ilet = arg
971 if isinstance(ilet, ChoosePeerCardInputlet):
972 self.do_shuffle(g, [ilet.target])
974 return arg
976 @staticmethod
977 def do_shuffle(g, pl):
978 log.debug('Shuffling cards')
980 for p in pl:
981 if not p.cards: continue
982 if any([c.is_card(VirtualCard) for c in p.cards]): 982 ↛ 983line 982 didn't jump to line 983, because the condition on line 982 was never true
983 log.warning('VirtualCard in cards of %s, not shuffling.' % repr(p))
984 continue
986 g.deck.shuffle(p.cards)
989class FatetellStage(GenericAction):
990 def __init__(self, target):
991 self.target = target
993 def apply_action(self):
994 g = self.game
995 target = self.target
996 if target.dead: return False
997 ft_cards = target.fatetell
998 while ft_cards:
999 if target.dead: break 999 ↛ 1003line 999 didn't jump to line 1003, because the break on line 999 wasn't executed
1000 card = ft_cards[-1] # what comes last, launches first.
1001 g.process_action(LaunchFatetellCard(target, card))
1003 return True
1006class BaseFatetell(GenericAction):
1007 def __init__(self, target, cond):
1008 self.source = None
1009 self.target = target
1010 self.cond = cond
1011 self.initiator = self.game.hybrid_stack[-1]
1012 self.card_manipulator = self
1014 def apply_action(self):
1015 g = self.game
1016 card, = g.deck.getcards(1)
1017 g.players.reveal(card)
1018 self.card = card
1019 detach_cards([card])
1020 g.emit_event(self.type, self)
1021 return self.succeeded
1023 def set_card(self, card, card_manipulator):
1024 self.card = card
1025 self.card_manipulator = card_manipulator
1027 @property
1028 def succeeded(self):
1029 # This is necessary, for ui
1030 return self.cond(self.card)
1033class Fatetell(BaseFatetell):
1034 type = 'fatetell'
1037class TurnOverCard(BaseFatetell):
1038 type = 'turnover'
1041class FatetellMalleateHandler(EventArbiter):
1042 interested = ['fatetell']
1044 def handle(self, evt_type, data):
1045 if evt_type != 'fatetell': return data 1045 ↛ exitline 1045 didn't return from function 'handle', because the return on line 1045 wasn't executed
1047 g = self.game
1048 try:
1049 tgt = PlayerTurn.get_current(g).target
1050 except IndexError:
1051 for a in g.action_stack:
1052 if isinstance(a, LaunchCard):
1053 tgt = a.source
1054 break
1055 else:
1056 # No one's turn
1057 # Only observed in `character_debut` events
1058 tgt = g.players[0]
1060 n = len(g.players)
1061 idx = g.players.index(tgt) - n
1062 for i in range(idx, idx + n):
1063 for eh in self.handlers:
1064 g.dispatcher.handle_single_event(eh, g.players[i], data)
1066 return data
1069class FatetellAction(GenericAction):
1070 fatetell_target = None
1072 def apply_action(self):
1073 assert self.fatetell_target is not None, 'Should specify fatetell_target!'
1075 ft = Fatetell(self.fatetell_target, self.fatetell_cond)
1076 g = self.game
1077 g.process_action(ft)
1079 if not ft.cancelled: 1079 ↛ 1089line 1079 didn't jump to line 1089, because the condition on line 1079 was never false
1080 rst = self.fatetell_action(ft)
1082 c = ft.card
1083 if (isinstance(c, PhysicalCard) and c.detached) or isinstance(c, VirtualCard):
1084 with MigrateCardsTransaction(ft.card_manipulator) as trans:
1085 migrate_cards([c], g.deck.droppedcards, unwrap=True, trans=trans)
1087 return rst
1089 return False
1091 def fatetell_action(self, ft):
1092 raise Exception('Implement fatetell_action!')
1094 def fatetell_cond(self, card):
1095 raise Exception('Implement fatetell_cond!')
1097 def fatetell_postprocess(self):
1098 pass
1101class LaunchFatetellCard(GenericAction):
1102 def __init__(self, target, card):
1103 self.source = None
1104 self.target = target
1105 self.card = card
1107 def apply_action(self):
1108 g = self.game
1109 target = self.target
1110 card = self.card
1111 act = card.delayed_action
1112 assert act
1113 a = act(source=target, target=target)
1114 a.associated_card = card
1115 self.card_action = a
1116 g.process_action(a)
1117 a.fatetell_postprocess()
1118 return True
1121class ForEach(UserAction):
1122 # action_cls == __subclass__.action_cls
1123 include_dead = False
1125 def prepare(self):
1126 pass
1128 def cleanup(self):
1129 pass
1131 def __init__(self, source, target):
1132 self.source = source
1133 self.target = None
1135 def apply_action(self):
1136 tl = self.target_list
1137 source = self.source
1138 card = self.associated_card
1139 g = self.game
1141 try:
1142 self.prepare()
1143 for t in tl:
1144 if t.dead and not self.include_dead:
1145 continue
1146 a = self.action_cls(source, t)
1147 a.associated_card = card
1148 a.parent_action = self
1149 g.process_action(a)
1151 finally:
1152 self.cleanup()
1154 return True
1156 @classmethod
1157 def get_actual_action(self, act):
1158 assert isinstance(act, Action)
1159 return getattr(act, 'parent_action', None)
1161 @classmethod
1162 def is_group_effect(self, act):
1163 return getattr(self.get_actual_action(act), 'group_effect', False)
1166class PrepareStage(GenericAction):
1167 def __init__(self, target):
1168 self.source = target
1169 self.target = target
1171 def apply_action(self):
1172 return True
1175class FinalizeStage(GenericAction):
1176 def __init__(self, target):
1177 self.source = target
1178 self.target = target
1180 def apply_action(self):
1181 return True
1184class PlayerTurn(GenericAction):
1185 def __init__(self, target: Character):
1186 self.source = self.target = target
1187 self.pending_stages = [
1188 PrepareStage,
1189 FatetellStage,
1190 DrawCardStage,
1191 ActionStage,
1192 DropCardStage,
1193 FinalizeStage,
1194 ]
1196 def apply_action(self) -> bool:
1197 g = self.game
1198 p = self.target
1199 p.tags['turn_count'] += 1
1201 while self.pending_stages:
1202 stage = self.pending_stages.pop(0)
1203 self.current_stage = cs = stage(p)
1204 g.process_action(cs)
1206 return True
1208 @staticmethod
1209 def get_current(g: THBattle) -> PlayerTurn:
1210 for act in g.action_stack:
1211 if isinstance(act, PlayerTurn):
1212 return act
1214 raise IndexError('Could not find current turn!')
1217class DummyAction(GenericAction):
1218 def __init__(self, source, target, result=True):
1219 self.source, self.target, self.result = \
1220 source, target, result
1222 def apply_action(self):
1223 return self.result
1226class RevealRole(THBPlayerAction):
1228 def __init__(self, role: PlayerRole, to: Union[Player, BatchList[Player]]):
1229 self.role = role
1230 self.to = to
1232 def apply_action(self) -> bool:
1233 self.to.reveal(self.role)
1234 return True
1236 def can_be_seen_by(self, ch: Character) -> bool:
1237 if isinstance(self.to, BatchList):
1238 return ch in self.to
1239 else:
1240 return ch is self.to
1243class Pindian(UserAction):
1244 no_reveal = True
1245 card_usage = 'pindian'
1247 def __init__(self, source, target):
1248 self.source = source
1249 self.target = target
1251 def apply_action(self) -> bool:
1252 src = self.source
1253 tgt = self.target
1254 g = self.game
1256 pl = BatchList([tgt, src])
1257 pindian_card: Dict[Character, Card] = {}
1259 with InputTransaction('Pindian', pl) as trans:
1260 for p in pl:
1261 cards = user_choose_cards(self, p, ('cards', 'showncards'), trans=trans)
1262 if cards: 1262 ↛ 1263line 1262 didn't jump to line 1263, because the condition on line 1262 was never true
1263 card = cards[0]
1264 else:
1265 card = random_choose_card(g, [p.cards, p.showncards])
1267 pindian_card[p] = card
1268 detach_cards([card])
1269 g.emit_event('pindian_card_chosen', (p, card))
1271 g.players.player.reveal([pindian_card[src], pindian_card[tgt]])
1272 g.emit_event('pindian_card_revealed', self) # for ui.
1273 migrate_cards([pindian_card[src], pindian_card[tgt]], g.deck.droppedcards, unwrap=True)
1275 return pindian_card[src].number > pindian_card[tgt].number
1277 @staticmethod
1278 def cond(cl):
1279 return len(cl) == 1 and \
1280 (not cl[0].is_card(Skill)) and \
1281 cl[0].resides_in.type in ('cards', 'showncards')
1283 def is_valid(self):
1284 src = self.source
1285 tgt = self.target
1286 if src.dead or tgt.dead: return False 1286 ↛ exitline 1286 didn't return from function 'is_valid', because the return on line 1286 wasn't executed
1287 if not (src.cards or src.showncards): return False
1288 if not (tgt.cards or tgt.showncards): return False
1289 return True
1292class Reforge(GenericAction):
1293 card_usage = 'reforge'
1295 def __init__(self, source, target, card):
1296 self.source = source
1297 self.target = target
1298 self.card = card
1300 def apply_action(self):
1301 g = self.game
1302 migrate_cards([self.card], g.deck.droppedcards, True)
1303 g.process_action(DrawCards(self.source, 1))
1305 return True
1307 def is_valid(self):
1308 return DrawCards(self.source, 1).can_fire()
1311@register_eh
1312class DyingHandler(THBEventHandler):
1313 interested = ['action_after']
1315 def handle(self, evt_type, act):
1316 if not evt_type == 'action_after': return act 1316 ↛ exitline 1316 didn't return from function 'handle', because the return on line 1316 wasn't executed
1317 if not isinstance(act, BaseDamage): return act
1319 src = act.source
1320 tgt = act.target
1321 if tgt.dead or tgt.life > 0: return act
1322 if tgt.tags['in_tryrevive']: 1322 ↛ 1325line 1322 didn't jump to line 1325, because the condition on line 1322 was never true
1323 # nested TryRevive, just return
1324 # will trigger when Eirin uses Diamond Exinwan to heal herself
1325 return act
1327 try:
1328 tgt.tags['in_tryrevive'] = True
1329 g = self.game
1330 if g.process_action(TryRevive(tgt, dmgact=act)):
1331 return act
1332 finally:
1333 tgt.tags['in_tryrevive'] = False
1335 g.process_action(PlayerDeath(src, tgt))
1337 return act
1340class ShowCards(GenericAction):
1341 def __init__(self, source, cards, to=None):
1342 self.source = self.target = source
1343 self.cards = cards
1344 self.to = to and BatchList(to)
1346 def apply_action(self):
1347 if not self.cards:
1348 return False
1350 g = self.game
1351 cards = self.cards
1352 to = self.to or g.players
1353 to.reveal(cards)
1354 g.emit_event('showcards', (self.target, [copy(c) for c in cards], to))
1355 # g.user_input(
1356 # [p for p in g.players if not p.dead],
1357 # ChooseOptionInputlet(self, (True,)),
1358 # type='all', timeout=1,
1359 # ) # just a delay
1360 return True
1363class ActionLimitExceeded(ActionShootdown):
1364 pass
1367class VitalityLimitExceeded(ActionLimitExceeded):
1368 pass