Coverage for thb/actions.py : 88%
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: 187 ↛ 188line 187 didn't jump to line 188, because the condition on line 187 was never true
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: 300 ↛ 301line 300 didn't jump to line 301, because the condition on line 300 was never true
301 continue
303 if isinstance(c, VirtualCard):
304 if not m.unwrap: 304 ↛ 305line 304 didn't jump to line 305, because the condition on line 304 was never true
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):
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': 327 ↛ 328line 327 didn't jump to line 328, because the condition on line 327 was never true
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)): 471 ↛ 472line 471 didn't jump to line 472, because the condition on line 471 was never true
472 if tgt.life > 0:
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: 570 ↛ 571line 570 didn't jump to line 571, because the condition on line 570 was never true
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: 579 ↛ 580line 579 didn't jump to line 580, because the condition on line 579 was never true
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: 638 ↛ 639line 638 didn't jump to line 639, because the condition on line 638 was never true
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): 654 ↛ 655line 654 didn't jump to line 655, because the condition on line 654 was never true
655 return False
657 from thb.cards.base import Skill
658 if any(c.is_card(Skill) for c in cards): 658 ↛ exit, 658 ↛ 6612 missed branches: 1) line 658 didn't finish the generator expression on line 658, 2) line 658 didn't jump to line 661, because the condition on line 658 was never false
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: 764 ↛ 765line 764 didn't jump to line 765, because the condition on line 764 was never true
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:
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 for p in pl:
979 if not p.cards: continue
980 if any([c.is_card(VirtualCard) for c in p.cards]): 980 ↛ 981line 980 didn't jump to line 981, because the condition on line 980 was never true
981 log.warning('VirtualCard in cards of %s, not shuffling.' % repr(p))
982 continue
984 g.deck.shuffle(p.cards)
987class FatetellStage(GenericAction):
988 def __init__(self, target):
989 self.target = target
991 def apply_action(self):
992 g = self.game
993 target = self.target
994 if target.dead: return False 994 ↛ exitline 994 didn't return from function 'apply_action', because the return on line 994 wasn't executed
995 ft_cards = target.fatetell
996 while ft_cards:
997 if target.dead: break 997 ↛ 1001line 997 didn't jump to line 1001, because the break on line 997 wasn't executed
998 card = ft_cards[-1] # what comes last, launches first.
999 g.process_action(LaunchFatetellCard(target, card))
1001 return True
1004class BaseFatetell(GenericAction):
1005 def __init__(self, target, cond):
1006 self.source = None
1007 self.target = target
1008 self.cond = cond
1009 self.initiator = self.game.hybrid_stack[-1]
1010 self.card_manipulator = self
1012 def apply_action(self):
1013 g = self.game
1014 card, = g.deck.getcards(1)
1015 g.players.reveal(card)
1016 self.card = card
1017 detach_cards([card])
1018 g.emit_event(self.type, self)
1019 return self.succeeded
1021 def set_card(self, card, card_manipulator):
1022 self.card = card
1023 self.card_manipulator = card_manipulator
1025 @property
1026 def succeeded(self):
1027 # This is necessary, for ui
1028 return self.cond(self.card)
1031class Fatetell(BaseFatetell):
1032 type = 'fatetell'
1035class TurnOverCard(BaseFatetell):
1036 type = 'turnover'
1039class FatetellMalleateHandler(EventArbiter):
1040 interested = ['fatetell']
1042 def handle(self, evt_type, data):
1043 if evt_type != 'fatetell': return data 1043 ↛ exitline 1043 didn't return from function 'handle', because the return on line 1043 wasn't executed
1045 g = self.game
1046 try:
1047 tgt = PlayerTurn.get_current(g).target
1048 except IndexError:
1049 for a in g.action_stack:
1050 if isinstance(a, LaunchCard):
1051 tgt = a.source
1052 break
1053 else:
1054 # No one's turn
1055 # Only observed in `character_debut` events
1056 tgt = g.players[0]
1058 n = len(g.players)
1059 idx = g.players.index(tgt) - n
1060 for i in range(idx, idx + n):
1061 for eh in self.handlers:
1062 g.dispatcher.handle_single_event(eh, g.players[i], data)
1064 return data
1067class FatetellAction(GenericAction):
1068 fatetell_target = None
1070 def apply_action(self):
1071 assert self.fatetell_target is not None, 'Should specify fatetell_target!'
1073 ft = Fatetell(self.fatetell_target, self.fatetell_cond)
1074 g = self.game
1075 g.process_action(ft)
1077 if not ft.cancelled: 1077 ↛ 1087line 1077 didn't jump to line 1087, because the condition on line 1077 was never false
1078 rst = self.fatetell_action(ft)
1080 c = ft.card
1081 if (isinstance(c, PhysicalCard) and c.detached) or isinstance(c, VirtualCard):
1082 with MigrateCardsTransaction(ft.card_manipulator) as trans:
1083 migrate_cards([c], g.deck.droppedcards, unwrap=True, trans=trans)
1085 return rst
1087 return False
1089 def fatetell_action(self, ft):
1090 raise Exception('Implement fatetell_action!')
1092 def fatetell_cond(self, card):
1093 raise Exception('Implement fatetell_cond!')
1095 def fatetell_postprocess(self):
1096 pass
1099class LaunchFatetellCard(GenericAction):
1100 def __init__(self, target, card):
1101 self.source = None
1102 self.target = target
1103 self.card = card
1105 def apply_action(self):
1106 g = self.game
1107 target = self.target
1108 card = self.card
1109 act = card.delayed_action
1110 assert act
1111 a = act(source=target, target=target)
1112 a.associated_card = card
1113 self.card_action = a
1114 g.process_action(a)
1115 a.fatetell_postprocess()
1116 return True
1119class ForEach(UserAction):
1120 # action_cls == __subclass__.action_cls
1121 include_dead = False
1123 def prepare(self):
1124 pass
1126 def cleanup(self):
1127 pass
1129 def __init__(self, source, target):
1130 self.source = source
1131 self.target = None
1133 def apply_action(self):
1134 tl = self.target_list
1135 source = self.source
1136 card = self.associated_card
1137 g = self.game
1139 try:
1140 self.prepare()
1141 for t in tl:
1142 if t.dead and not self.include_dead: 1142 ↛ 1143line 1142 didn't jump to line 1143, because the condition on line 1142 was never true
1143 continue
1144 a = self.action_cls(source, t)
1145 a.associated_card = card
1146 a.parent_action = self
1147 g.process_action(a)
1149 finally:
1150 self.cleanup()
1152 return True
1154 @classmethod
1155 def get_actual_action(self, act):
1156 assert isinstance(act, Action)
1157 return getattr(act, 'parent_action', None)
1159 @classmethod
1160 def is_group_effect(self, act):
1161 return getattr(self.get_actual_action(act), 'group_effect', False)
1164class PrepareStage(GenericAction):
1165 def __init__(self, target):
1166 self.source = target
1167 self.target = target
1169 def apply_action(self):
1170 return True
1173class FinalizeStage(GenericAction):
1174 def __init__(self, target):
1175 self.source = target
1176 self.target = target
1178 def apply_action(self):
1179 return True
1182class PlayerTurn(GenericAction):
1183 def __init__(self, target: Character):
1184 self.source = self.target = target
1185 self.pending_stages = [
1186 PrepareStage,
1187 FatetellStage,
1188 DrawCardStage,
1189 ActionStage,
1190 DropCardStage,
1191 FinalizeStage,
1192 ]
1194 def apply_action(self) -> bool:
1195 g = self.game
1196 p = self.target
1197 p.tags['turn_count'] += 1
1199 while self.pending_stages:
1200 stage = self.pending_stages.pop(0)
1201 self.current_stage = cs = stage(p)
1202 g.process_action(cs)
1204 return True
1206 @staticmethod
1207 def get_current(g: THBattle) -> PlayerTurn:
1208 for act in g.action_stack:
1209 if isinstance(act, PlayerTurn):
1210 return act
1212 raise IndexError('Could not find current turn!')
1215class DummyAction(GenericAction):
1216 def __init__(self, source, target, result=True):
1217 self.source, self.target, self.result = \
1218 source, target, result
1220 def apply_action(self):
1221 return self.result
1224class RevealRole(THBPlayerAction):
1226 def __init__(self, role: PlayerRole, to: Union[Player, BatchList[Player]]):
1227 self.role = role
1228 self.to = to
1230 def apply_action(self) -> bool:
1231 self.to.reveal(self.role)
1232 return True
1234 def can_be_seen_by(self, ch: Character) -> bool:
1235 if isinstance(self.to, BatchList):
1236 return ch in self.to
1237 else:
1238 return ch is self.to
1241class Pindian(UserAction):
1242 no_reveal = True
1243 card_usage = 'pindian'
1245 def __init__(self, source, target):
1246 self.source = source
1247 self.target = target
1249 def apply_action(self) -> bool:
1250 src = self.source
1251 tgt = self.target
1252 g = self.game
1254 pl = BatchList([tgt, src])
1255 pindian_card: Dict[Character, Card] = {}
1257 with InputTransaction('Pindian', pl) as trans:
1258 for p in pl:
1259 cards = user_choose_cards(self, p, ('cards', 'showncards'), trans=trans)
1260 if cards: 1260 ↛ 1261line 1260 didn't jump to line 1261, because the condition on line 1260 was never true
1261 card = cards[0]
1262 else:
1263 card = random_choose_card(g, [p.cards, p.showncards])
1265 pindian_card[p] = card
1266 detach_cards([card])
1267 g.emit_event('pindian_card_chosen', (p, card))
1269 g.players.player.reveal([pindian_card[src], pindian_card[tgt]])
1270 g.emit_event('pindian_card_revealed', self) # for ui.
1271 migrate_cards([pindian_card[src], pindian_card[tgt]], g.deck.droppedcards, unwrap=True)
1273 return pindian_card[src].number > pindian_card[tgt].number
1275 @staticmethod
1276 def cond(cl):
1277 return len(cl) == 1 and \
1278 (not cl[0].is_card(Skill)) and \
1279 cl[0].resides_in.type in ('cards', 'showncards')
1281 def is_valid(self):
1282 src = self.source
1283 tgt = self.target
1284 if src.dead or tgt.dead: return False 1284 ↛ exitline 1284 didn't return from function 'is_valid', because the return on line 1284 wasn't executed
1285 if not (src.cards or src.showncards): return False 1285 ↛ exitline 1285 didn't return from function 'is_valid', because the return on line 1285 wasn't executed
1286 if not (tgt.cards or tgt.showncards): return False
1287 return True
1290class Reforge(GenericAction):
1291 card_usage = 'reforge'
1293 def __init__(self, source, target, card):
1294 self.source = source
1295 self.target = target
1296 self.card = card
1298 def apply_action(self):
1299 g = self.game
1300 migrate_cards([self.card], g.deck.droppedcards, True)
1301 g.process_action(DrawCards(self.source, 1))
1303 return True
1305 def is_valid(self):
1306 return DrawCards(self.source, 1).can_fire()
1309@register_eh
1310class DyingHandler(THBEventHandler):
1311 interested = ['action_after']
1313 def handle(self, evt_type, act):
1314 if not evt_type == 'action_after': return act 1314 ↛ exitline 1314 didn't return from function 'handle', because the return on line 1314 wasn't executed
1315 if not isinstance(act, BaseDamage): return act
1317 src = act.source
1318 tgt = act.target
1319 if tgt.dead or tgt.life > 0: return act
1320 if tgt.tags['in_tryrevive']: 1320 ↛ 1323line 1320 didn't jump to line 1323, because the condition on line 1320 was never true
1321 # nested TryRevive, just return
1322 # will trigger when Eirin uses Diamond Exinwan to heal herself
1323 return act
1325 try:
1326 tgt.tags['in_tryrevive'] = True
1327 g = self.game
1328 if g.process_action(TryRevive(tgt, dmgact=act)):
1329 return act
1330 finally:
1331 tgt.tags['in_tryrevive'] = False
1333 g.process_action(PlayerDeath(src, tgt))
1335 return act
1338class ShowCards(GenericAction):
1339 def __init__(self, source, cards, to=None):
1340 self.source = self.target = source
1341 self.cards = cards
1342 self.to = to and BatchList(to)
1344 def apply_action(self):
1345 if not self.cards: 1345 ↛ 1346line 1345 didn't jump to line 1346, because the condition on line 1345 was never true
1346 return False
1348 g = self.game
1349 cards = self.cards
1350 to = self.to or g.players
1351 to.reveal(cards)
1352 g.emit_event('showcards', (self.target, [copy(c) for c in cards], to))
1353 # g.user_input(
1354 # [p for p in g.players if not p.dead],
1355 # ChooseOptionInputlet(self, (True,)),
1356 # type='all', timeout=1,
1357 # ) # just a delay
1358 return True
1361class ActionLimitExceeded(ActionShootdown):
1362 pass
1365class VitalityLimitExceeded(ActionLimitExceeded):
1366 pass