# Coverage for thb/actions.py : 92%

## 883 statements 825 run 58 missing 5 excluded 26 partial

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 -*-

2 from __future__ import annotations

3

4# -- stdlib --

5 from collections import defaultdict

6 from copy import copy

7 from dataclasses import dataclass

8 from typing import Any, Dict, List, Optional, Sequence, Set, TYPE_CHECKING, Tuple, Type, Union, cast

9 import logging

10

11# -- third party --

12 from typing_extensions import Literal, Protocol

13

14# -- own --

15 from game.base import Action, ActionShootdown, EventArbiter, GameViralContext, InputTransaction

16 from game.base import Player, sync_primitive

17 from thb.cards.base import Card, CardList, PhysicalCard, Skill, VirtualCard

18 from thb.common import PlayerRole

19 from thb.inputlets import ActionInputlet, ChoosePeerCardInputlet

20 from thb.mode import THBAction, THBEventHandler, THBPlayerAction, THBattle

21 from utils.check import CheckFailed, check, check_type

22 from utils.misc import BatchList

23

24# -- typing --

25 if TYPE_CHECKING:

26 from thb.characters.base import Character # noqa: F401

27

28

29# -- code --

30 log = logging.getLogger('THBattle_Actions')

31

32

33# ------------------------------------------

34# aux functions

35

36 def ttags(actor):

37 tags = actor.tags

38 tc = tags['turn_count']

39 return tags.setdefault('turn_tags:%s' % tc, defaultdict(int))

40

41

42 class CardChooser(Protocol):

43 game: THBattle

44 card_usage: str

45

46 def cond(self, cards: Sequence[Card]) -> bool: ... 46 ↛ exit line 46 didn't return from function 'cond'

47

48

49 class CharacterChooser(Protocol):

50 game: THBattle

51

52 def choose_player_target(self, pl: Sequence[Character]) -> Tuple[List[Character], bool]: ... 52 ↛ exit line 52 didn't return from function 'choose_player_target'

53

54

55 def 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

66

67 assert categories or candidates

68 assert actors

69

70 timeout = timeout or 25

71

72 from thb.cards.base import VirtualCard

73

74 g = cast(THBattle, initiator.game)

75

76 ilet = ActionInputlet(initiator, categories, candidates)

77

78 @ilet.with_post_process

79 def process(actor: Character, rst):

80 usage = getattr(initiator, 'card_usage', 'none')

81 try:

82 check(rst)

83

84 skills: List[Type[Skill]]

85 rawcards: List[Card]

86 characters: List[Character]

87 params: Dict[str, Any]

88

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

92

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'

100

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

106

107 c = cast(VirtualCard, c)

108

109 g.players.player.reveal(c.associated_cards)

110 for c1 in c.associated_cards:

111 walk(c1)

112

113 walk(cards[0])

114 check(skill_check(cards[0]))

115

116 else:

117 if not getattr(initiator, 'no_reveal', False):

118 g.players.player.reveal(cards)

119

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 = []

124

125 if candidates:

126 characters, valid = cast(CharacterChooser, initiator).choose_player_target(characters)

127 check(valid)

128

129 ask_for_action_verify = getattr(initiator, 'ask_for_action_verify', None)

130

131 if ask_for_action_verify:

132 check(ask_for_action_verify(actor, cards, characters))

133

134 return cards, characters, params

135

136 except CheckFailed:

137 return None

138

139 p, rst = g.user_input(actors, ilet, timeout=timeout, type='any', trans=trans)

140 if rst:

141 cards, characters, params = rst

142

143 if len(cards) == 1 and cards[0].is_card(VirtualCard):

144 g.deck.register_vcard(cards[0])

145

146 if not cards and not characters: 146 ↛ 147 line 146 didn't jump to line 147, because the condition on line 146 was never true

147 return p, None

148

149 [c.detach() for c in VirtualCard.unwrap(cards)]

150

151 return p, (cards, characters)

152 else:

153 return None, None

154

155

156 def 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)

163

164 _, rst = ask_for_action(initiator, [actor], categories, (), timeout=timeout, trans=trans)

165 if not rst:

166 return None

167

168 return rst[0] # cards

169

170

171 def 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

180

181 return rst[1] # players

182

183

184 def 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

189

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

196

197

198 def 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]

203

204 return cards[0]

205

206

207 def skill_check(wrapped):

208 from thb.cards.base import Skill

209

210 try:

211 check(wrapped.check())

212 for c in wrapped.associated_cards:

213 if c.is_card(Skill): 213 ↛ 214 line 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)

218

219 return True

220

221 except CheckFailed:

222 return False

223

224

225 COMMON_EVENT_HANDLERS: Set[Type[THBEventHandler]] = set()

226

227

228 def register_eh(cls):

229 COMMON_EVENT_HANDLERS.add(cls)

230 return cls

231

232

233# ------------------------------------------

234 class GenericAction(THBAction):

235 pass

236

237

238 class UserAction(THBAction): # card/character skill actions

239 target_list: Sequence[Character]

240 associated_card: Card

241

242

243@dataclass

244 class 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'

250

251

252@dataclass

253 class CardMovement:

254 trans: MigrateCardsTransaction

255 card: Card

256 fr: CardList

257 to: CardList

258 direction: Literal['front', 'back'] = 'front'

259

260

261 class MigrateCardsTransaction(GameViralContext):

262 movements: List[CardMovement]

263

264 def __init__(self, action: Optional[THBAction] = None):

265 g = self.game

266 self.action = cast(THBAction, action or g.action_stack[-1])

267

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])

271

272 self.cancelled = False

273 self.migrations = []

274 self.movements = []

275

276 def __repr__(self):

277 return 'MCT'

278

279 def add_step(self, m: CardMigration) -> None:

280 self.migrations.append(m)

281

282 def __enter__(self):

283 return self

284

285 def __exit__(self, *excinfo):

286 if not self.cancelled:

287 self.commit()

288 else:

289 log.debug('migrate_cards cancelled: %s', self.movements)

290

291 def commit(self):

292 g = self.game

293

294 seen = set()

295

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

302

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 ↛ 307 line 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

319

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

326

327 if m.direction == 'back':

328 m.to.rotate(n)

329

330 g.emit_event('post_card_migration', self)

331

332

333 def 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

349

350 assert to is not None

351

352 if to.owner and to.owner.dead:

353 # do not migrate cards to dead character

354 trans.cancelled = True

355 return

356

357 trans.add_step(CardMigration(

358 trans=trans,

359 cards=cards,

360 to=to,

361 unwrap=unwrap,

362 direction=direction,

363 ))

364

365

366 class PostCardMigrationHandler(EventArbiter):

367 interested = ['post_card_migration']

368

369 def handle(self, evt_type, arg):

370 if evt_type != 'post_card_migration': return arg 370 ↛ exit line 370 didn't return from function 'handle', because the return on line 370 wasn't executed

371

372 g = self.game

373 act = arg.action

374 tgt = act.target or act.source or g.players[0]

375

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)

385

386 return arg

387

388

389 def 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)

394

395

396 class DeadDropCards(GenericAction):

397 def apply_action(self):

398 tgt = self.target

399 g = self.game

400

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

409

410 return True

411

412

413 class 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

423

424

425 class PlayerRevive(GenericAction):

426 def __init__(self, source, target, hp):

427 self.source = source

428 self.target = target

429 self.hp = hp

430

431 def apply_action(self):

432 tgt = self.target

433 assert tgt.dead

434

435 tgt.dead = False

436 tgt.maxlife = tgt.__class__.maxlife

437 tgt.skills = list(tgt.__class__.skills)

438

439 tgt.life = min(tgt.maxlife, self.hp)

440 return True

441

442 def is_valid(self):

443 return self.target.dead

444

445

446 class 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 ↛ 452 line 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

454

455 def apply_action(self) -> bool:

456 tgt = self.target

457

458 if tgt.dead: 458 ↛ 459 line 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

463

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

470

471 if g.process_action(AskForHeal(tgt, p)):

472 if tgt.life > 0: 472 ↛ 475 line 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

476

477 break

478

479 return tgt.life > 0

480

481 def is_valid(self):

482 tgt = self.target

483 return not tgt.dead and tgt.maxlife > 0

484

485

486 class BaseDamage(GenericAction):

487 def __init__(self, source, target, amount=1):

488 self.source = source

489 self.target = target

490 self.amount = amount

491

492 def apply_action(self):

493 tgt = self.target

494 tgt.life -= self.amount

495 return True

496

497 def is_valid(self):

498 return not self.target.dead

499

500

501 class Damage(BaseDamage):

502 pass

503

504

505 class LifeLost(BaseDamage):

506 def __init__(self, source, target, amount=1):

507 self.source = None

508 self.target = target

509 self.amount = amount

510

511

512 class MaxLifeChange(GenericAction):

513 def __init__(self, source, target, amount):

514 self.source = source

515 self.target = target

516 self.amount = amount

517

518 def apply_action(self):

519 src = self.source

520 tgt = self.target

521 g = self.game

522 tgt.maxlife += self.amount

523

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

529

530 assert tgt.maxlife or tgt.dead

531

532 return True

533

534 def is_valid(self):

535 return not self.target.dead

536

537

538# ---------------------------------------------------

539

540 class DropCards(GenericAction):

541 def __init__(self, source, target, cards):

542 self.source = source

543 self.target = target

544 self.cards = cards

545

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)

552

553 return True

554

555 def is_valid(self):

556 return True

557

558

559 class UseCard(GenericAction):

560

561 def __init__(self, target, card):

562 self.source = self.target = target

563 self.card = card

564

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

575

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

583

584

585 class AskForCard(GenericAction):

586

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

592

593 self.card: Optional[Card] = None

594

595 def apply_action(self):

596 target = self.target

597

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

603

604 self.card = cards[0]

605

606 return self.process_card(self.card)

607

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 )

616

617 def process_card(self, card):

618 raise NotImplementedError

619

620

621 class ActiveDropCards(GenericAction):

622 card_usage = 'drop'

623

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] = []

629

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

635

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))

645

646 self.cards = cards

647 return True

648

649 def cond(self, cards: Sequence[Card]) -> bool:

650 tgt = self.target

651 if not len(cards) == self.dropn:

652 return False

653

654 if not all(c.resides_in in (tgt.cards, tgt.showncards) for c in cards):

655 return False

656

657 from thb.cards.base import Skill

658 if any(c.is_card(Skill) for c in cards):

659 return False

660

661 return True

662

663

664 class DropCardStage(ActiveDropCards):

665 def __init__(self, target):

666 dropn = len(target.cards) + len(target.showncards) - target.life

667 ActiveDropCards.__init__(self, target, target, dropn)

668

669

670 class 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

675

676 def apply_action(self):

677 g = self.game

678 target = self.target

679

680 if self.back: 680 ↛ 681 line 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)

683

684 cards = g.deck.getcards(self.amount)

685

686 target.reveal(cards)

687 migrate_cards(cards, target.cards)

688 self.cards = cards

689 return True

690

691 def is_valid(self):

692 return not self.target.dead

693

694

695 class DrawCards(BaseDrawCards):

696 pass

697

698

699 class DistributeCards(BaseDrawCards):

700 pass

701

702

703 class DrawCardStage(DrawCards):

704 pass

705

706

707 class PutBack(GenericAction):

708 def __init__(self, source, cards, front=True):

709 self.source = self.target = source

710 self.cards = cards

711 self.front = front

712

713 def apply_action(self):

714 g = self.game

715 cards = self.cards

716

717 migrate_cards(cards, g.deck.cards, front=self.front)

718

719 return True

720

721

722 class 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)

735

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

738

739 def apply_action(self) -> bool:

740 card = self.card

741 target_list = self.target_list

742 if not card: return False 742 ↛ exit line 742 didn't return from function 'apply_action', because the return on line 742 wasn't executed

743

744 if not self.force_action and not card.associated_action: 744 ↛ 745 line 744 didn't jump to line 745, because the condition on line 744 was never true

745 return False

746

747 g = self.game

748 src = self.source

749 drop = card.usage == 'drop'

750 try:

751 if drop: # should drop before action 751 ↛ 752 line 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]))

753

754 elif not getattr(card, 'no_drop', False):

755 detach_cards([card]) # emit events

756

757 else:

758 card.detach()

759

760 _, tl = g.emit_event('choose_target', (self, target_list))

761 assert _ is self

762 card = self.card # card may be touched

763

764 if not tl:

765 return True

766

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

774

775 a.associated_card = card

776 self.card_action = a

777

778 _ = g.emit_event('post_choose_target', (self, tl))

779 card = self.card # card may be touched

780 assert _ == (self, tl)

781

782 g.process_action(a)

783 card = self.card # card may be touched

784

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

790

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)

797

798 else:

799 from thb.cards.base import VirtualCard

800 for c in VirtualCard.unwrap([card]):

801 if c.detached: c.attach()

802

803 return True

804

805 def is_valid(self) -> bool:

806 if self.bypass_check:

807 return True

808

809 if not self.tl_valid:

810 log.debug('LaunchCard.tl_valid FALSE')

811 return False

812

813 card = self.card

814 if not card: 814 ↛ 815 line 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

817

818 g = self.game

819 src = self.source

820

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

825

826 cls = card.associated_action

827 if not cls: 827 ↛ 828 line 827 didn't jump to line 828, because the condition on line 827 was never true

828 return False

829

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

840

841 return True

842

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

851

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

857

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

868

869

870 class ActionStageLaunchCard(LaunchCard):

871 pass

872

873

874 class BaseActionStage(GenericAction):

875 card_usage = 'launch'

876 launch_card_cls: Type[LaunchCard]

877

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

884

885 def apply_action(self):

886 g = self.game

887 target = self.target

888 if target.dead: return False

889

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

903

904 cards, target_list = rst

905 g.players.reveal(cards)

906 card = cards[0]

907

908 lc = self.launch_card_cls(target, target_list, card)

909 if not g.process_action(lc):

910 if lc.invalid: 910 ↛ 911 line 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)

913

914 self.action_count += 1

915

916 if self.one_shot or self._force_break:

917 break

918

919 except CheckFailed:

920 pass

921

922 return True

923

924 @staticmethod

925 def force_break(g):

926 for a in g.action_stack: 926 ↛ exit line 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

931

932 def cond(self, cl):

933 if not cl: return False

934

935 tgt = self.target

936 if not len(cl) == 1: 936 ↛ 937 line 936 didn't jump to line 937, because the condition on line 936 was never true

937 return False

938

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)

943

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()

947

948 def choose_player_target(self, tl):

949 return tl, True

950

951

952 class ActionStage(BaseActionStage):

953 one_shot = False

954 launch_card_cls = ActionStageLaunchCard

955

956

957@register_eh

958 class ShuffleHandler(THBEventHandler):

959 interested = ['action_after', 'action_before', 'action_stage_action', 'user_input_start']

960

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)

965

966 elif evt_type in ('action_before', 'action_after') and isinstance(arg, ActionStage):

967 self.do_shuffle(g, g.players)

968

969 elif evt_type == 'user_input_start':

970 trans, ilet = arg

971 if isinstance(ilet, ChoosePeerCardInputlet):

972 self.do_shuffle(g, [ilet.target])

973

974 return arg

975

976 @staticmethod

977 def do_shuffle(g, pl):

978 log.debug('Shuffling cards')

979

980 for p in pl:

981 if not p.cards: continue

982 if any([c.is_card(VirtualCard) for c in p.cards]): 982 ↛ 983 line 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

985

986 g.deck.shuffle(p.cards)

987

988

989 class FatetellStage(GenericAction):

990 def __init__(self, target):

991 self.target = target

992

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 ↛ 1003 line 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))

1002

1003 return True

1004

1005

1006 class 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

1013

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

1022

1023 def set_card(self, card, card_manipulator):

1024 self.card = card

1025 self.card_manipulator = card_manipulator

1026

1027 @property

1028 def succeeded(self):

1029 # This is necessary, for ui

1030 return self.cond(self.card)

1031

1032

1033 class Fatetell(BaseFatetell):

1034 type = 'fatetell'

1035

1036

1037 class TurnOverCard(BaseFatetell):

1038 type = 'turnover'

1039

1040

1041 class FatetellMalleateHandler(EventArbiter):

1042 interested = ['fatetell']

1043

1044 def handle(self, evt_type, data):

1045 if evt_type != 'fatetell': return data 1045 ↛ exit line 1045 didn't return from function 'handle', because the return on line 1045 wasn't executed

1046

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]

1059

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)

1065

1066 return data

1067

1068

1069 class FatetellAction(GenericAction):

1070 fatetell_target = None

1071

1072 def apply_action(self):

1073 assert self.fatetell_target is not None, 'Should specify fatetell_target!'

1074

1075 ft = Fatetell(self.fatetell_target, self.fatetell_cond)

1076 g = self.game

1077 g.process_action(ft)

1078

1079 if not ft.cancelled: 1079 ↛ 1089 line 1079 didn't jump to line 1089, because the condition on line 1079 was never false

1080 rst = self.fatetell_action(ft)

1081

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)

1086

1087 return rst

1088

1089 return False

1090

1091 def fatetell_action(self, ft):

1092 raise Exception('Implement fatetell_action!')

1093

1094 def fatetell_cond(self, card):

1095 raise Exception('Implement fatetell_cond!')

1096

1097 def fatetell_postprocess(self):

1098 pass

1099

1100

1101 class LaunchFatetellCard(GenericAction):

1102 def __init__(self, target, card):

1103 self.source = None

1104 self.target = target

1105 self.card = card

1106

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

1119

1120

1121 class ForEach(UserAction):

1122 # action_cls == __subclass__.action_cls

1123 include_dead = False

1124

1125 def prepare(self):

1126 pass

1127

1128 def cleanup(self):

1129 pass

1130

1131 def __init__(self, source, target):

1132 self.source = source

1133 self.target = None

1134

1135 def apply_action(self):

1136 tl = self.target_list

1137 source = self.source

1138 card = self.associated_card

1139 g = self.game

1140

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)

1150

1151 finally:

1152 self.cleanup()

1153

1154 return True

1155

1156 @classmethod

1157 def get_actual_action(self, act):

1158 assert isinstance(act, Action)

1159 return getattr(act, 'parent_action', None)

1160

1161 @classmethod

1162 def is_group_effect(self, act):

1163 return getattr(self.get_actual_action(act), 'group_effect', False)

1164

1165

1166 class PrepareStage(GenericAction):

1167 def __init__(self, target):

1168 self.source = target

1169 self.target = target

1170

1171 def apply_action(self):

1172 return True

1173

1174

1175 class FinalizeStage(GenericAction):

1176 def __init__(self, target):

1177 self.source = target

1178 self.target = target

1179

1180 def apply_action(self):

1181 return True

1182

1183

1184 class 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 ]

1195

1196 def apply_action(self) -> bool:

1197 g = self.game

1198 p = self.target

1199 p.tags['turn_count'] += 1

1200

1201 while self.pending_stages:

1202 stage = self.pending_stages.pop(0)

1203 self.current_stage = cs = stage(p)

1204 g.process_action(cs)

1205

1206 return True

1207

1208 @staticmethod

1209 def get_current(g: THBattle) -> PlayerTurn:

1210 for act in g.action_stack:

1211 if isinstance(act, PlayerTurn):

1212 return act

1213

1214 raise IndexError('Could not find current turn!')

1215

1216

1217 class DummyAction(GenericAction):

1218 def __init__(self, source, target, result=True):

1219 self.source, self.target, self.result = \

1220 source, target, result

1221

1222 def apply_action(self):

1223 return self.result

1224

1225

1226 class RevealRole(THBPlayerAction):

1227

1228 def __init__(self, role: PlayerRole, to: Union[Player, BatchList[Player]]):

1229 self.role = role

1230 self.to = to

1231

1232 def apply_action(self) -> bool:

1233 self.to.reveal(self.role)

1234 return True

1235

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

1241

1242

1243 class Pindian(UserAction):

1244 no_reveal = True

1245 card_usage = 'pindian'

1246

1247 def __init__(self, source, target):

1248 self.source = source

1249 self.target = target

1250

1251 def apply_action(self) -> bool:

1252 src = self.source

1253 tgt = self.target

1254 g = self.game

1255

1256 pl = BatchList([tgt, src])

1257 pindian_card: Dict[Character, Card] = {}

1258

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 ↛ 1263 line 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])

1266

1267 pindian_card[p] = card

1268 detach_cards([card])

1269 g.emit_event('pindian_card_chosen', (p, card))

1270

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)

1274

1275 return pindian_card[src].number > pindian_card[tgt].number

1276

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')

1282

1283 def is_valid(self):

1284 src = self.source

1285 tgt = self.target

1286 if src.dead or tgt.dead: return False 1286 ↛ exit line 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

1290

1291

1292 class Reforge(GenericAction):

1293 card_usage = 'reforge'

1294

1295 def __init__(self, source, target, card):

1296 self.source = source

1297 self.target = target

1298 self.card = card

1299

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))

1304

1305 return True

1306

1307 def is_valid(self):

1308 return DrawCards(self.source, 1).can_fire()

1309

1310

1311@register_eh

1312 class DyingHandler(THBEventHandler):

1313 interested = ['action_after']

1314

1315 def handle(self, evt_type, act):

1316 if not evt_type == 'action_after': return act 1316 ↛ exit line 1316 didn't return from function 'handle', because the return on line 1316 wasn't executed

1317 if not isinstance(act, BaseDamage): return act

1318

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 ↛ 1325 line 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

1326

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

1334

1335 g.process_action(PlayerDeath(src, tgt))

1336

1337 return act

1338

1339

1340 class 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)

1345

1346 def apply_action(self):

1347 if not self.cards:

1348 return False

1349

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

1361

1362

1363 class ActionLimitExceeded(ActionShootdown):

1364 pass

1365

1366

1367 class VitalityLimitExceeded(ActionLimitExceeded):

1368 pass