Hide keyboard shortcuts

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 

3 

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 

10 

11# -- third party -- 

12from typing_extensions import Literal, Protocol 

13 

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 

23 

24# -- typing -- 

25if TYPE_CHECKING: 

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

27 

28 

29# -- code -- 

30log = logging.getLogger('THBattle_Actions') 

31 

32 

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

34# aux functions 

35 

36def ttags(actor): 

37 tags = actor.tags 

38 tc = tags['turn_count'] 

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

40 

41 

42class CardChooser(Protocol): 

43 game: THBattle 

44 card_usage: str 

45 

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

47 

48 

49class CharacterChooser(Protocol): 

50 game: THBattle 

51 

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

53 

54 

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 

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

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) 

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 

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 

180 

181 return rst[1] # players 

182 

183 

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 

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 

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] 

203 

204 return cards[0] 

205 

206 

207def 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 ↛ 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) 

218 

219 return True 

220 

221 except CheckFailed: 

222 return False 

223 

224 

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

226 

227 

228def register_eh(cls): 

229 COMMON_EVENT_HANDLERS.add(cls) 

230 return cls 

231 

232 

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

234class GenericAction(THBAction): 

235 pass 

236 

237 

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

239 target_list: Sequence[Character] 

240 associated_card: Card 

241 

242 

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' 

250 

251 

252@dataclass 

253class CardMovement: 

254 trans: MigrateCardsTransaction 

255 card: Card 

256 fr: CardList 

257 to: CardList 

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

259 

260 

261class 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: 300 ↛ 301line 300 didn't jump to line 301, because the condition on line 300 was never true

301 continue 

302 

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 

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': 327 ↛ 328line 327 didn't jump to line 328, because the condition on line 327 was never true

328 m.to.rotate(n) 

329 

330 g.emit_event('post_card_migration', self) 

331 

332 

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 

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 

366class 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 ↛ exitline 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 

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) 

394 

395 

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

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 

423 

424 

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

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 

454 

455 def apply_action(self) -> bool: 

456 tgt = self.target 

457 

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 

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

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 

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

501class Damage(BaseDamage): 

502 pass 

503 

504 

505class LifeLost(BaseDamage): 

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

507 self.source = None 

508 self.target = target 

509 self.amount = amount 

510 

511 

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

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

559class 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: 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 

575 

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 

583 

584 

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

621class 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: 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)) 

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): 654 ↛ 655line 654 didn't jump to line 655, because the condition on line 654 was never true

655 return False 

656 

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 

660 

661 return True 

662 

663 

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) 

668 

669 

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 

675 

676 def apply_action(self): 

677 g = self.game 

678 target = self.target 

679 

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) 

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 

695class DrawCards(BaseDrawCards): 

696 pass 

697 

698 

699class DistributeCards(BaseDrawCards): 

700 pass 

701 

702 

703class DrawCardStage(DrawCards): 

704 pass 

705 

706 

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 

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 

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) 

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

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: 764 ↛ 765line 764 didn't jump to line 765, because the condition on line 764 was never true

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

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

870class ActionStageLaunchCard(LaunchCard): 

871 pass 

872 

873 

874class 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 ↛ 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) 

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: 

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

952class ActionStage(BaseActionStage): 

953 one_shot = False 

954 launch_card_cls = ActionStageLaunchCard 

955 

956 

957@register_eh 

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

983 

984 g.deck.shuffle(p.cards) 

985 

986 

987class FatetellStage(GenericAction): 

988 def __init__(self, target): 

989 self.target = target 

990 

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

1000 

1001 return True 

1002 

1003 

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 

1011 

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 

1020 

1021 def set_card(self, card, card_manipulator): 

1022 self.card = card 

1023 self.card_manipulator = card_manipulator 

1024 

1025 @property 

1026 def succeeded(self): 

1027 # This is necessary, for ui 

1028 return self.cond(self.card) 

1029 

1030 

1031class Fatetell(BaseFatetell): 

1032 type = 'fatetell' 

1033 

1034 

1035class TurnOverCard(BaseFatetell): 

1036 type = 'turnover' 

1037 

1038 

1039class FatetellMalleateHandler(EventArbiter): 

1040 interested = ['fatetell'] 

1041 

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

1044 

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] 

1057 

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) 

1063 

1064 return data 

1065 

1066 

1067class FatetellAction(GenericAction): 

1068 fatetell_target = None 

1069 

1070 def apply_action(self): 

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

1072 

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

1074 g = self.game 

1075 g.process_action(ft) 

1076 

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) 

1079 

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) 

1084 

1085 return rst 

1086 

1087 return False 

1088 

1089 def fatetell_action(self, ft): 

1090 raise Exception('Implement fatetell_action!') 

1091 

1092 def fatetell_cond(self, card): 

1093 raise Exception('Implement fatetell_cond!') 

1094 

1095 def fatetell_postprocess(self): 

1096 pass 

1097 

1098 

1099class LaunchFatetellCard(GenericAction): 

1100 def __init__(self, target, card): 

1101 self.source = None 

1102 self.target = target 

1103 self.card = card 

1104 

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 

1117 

1118 

1119class ForEach(UserAction): 

1120 # action_cls == __subclass__.action_cls 

1121 include_dead = False 

1122 

1123 def prepare(self): 

1124 pass 

1125 

1126 def cleanup(self): 

1127 pass 

1128 

1129 def __init__(self, source, target): 

1130 self.source = source 

1131 self.target = None 

1132 

1133 def apply_action(self): 

1134 tl = self.target_list 

1135 source = self.source 

1136 card = self.associated_card 

1137 g = self.game 

1138 

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) 

1148 

1149 finally: 

1150 self.cleanup() 

1151 

1152 return True 

1153 

1154 @classmethod 

1155 def get_actual_action(self, act): 

1156 assert isinstance(act, Action) 

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

1158 

1159 @classmethod 

1160 def is_group_effect(self, act): 

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

1162 

1163 

1164class PrepareStage(GenericAction): 

1165 def __init__(self, target): 

1166 self.source = target 

1167 self.target = target 

1168 

1169 def apply_action(self): 

1170 return True 

1171 

1172 

1173class FinalizeStage(GenericAction): 

1174 def __init__(self, target): 

1175 self.source = target 

1176 self.target = target 

1177 

1178 def apply_action(self): 

1179 return True 

1180 

1181 

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 ] 

1193 

1194 def apply_action(self) -> bool: 

1195 g = self.game 

1196 p = self.target 

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

1198 

1199 while self.pending_stages: 

1200 stage = self.pending_stages.pop(0) 

1201 self.current_stage = cs = stage(p) 

1202 g.process_action(cs) 

1203 

1204 return True 

1205 

1206 @staticmethod 

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

1208 for act in g.action_stack: 

1209 if isinstance(act, PlayerTurn): 

1210 return act 

1211 

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

1213 

1214 

1215class DummyAction(GenericAction): 

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

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

1218 source, target, result 

1219 

1220 def apply_action(self): 

1221 return self.result 

1222 

1223 

1224class RevealRole(THBPlayerAction): 

1225 

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

1227 self.role = role 

1228 self.to = to 

1229 

1230 def apply_action(self) -> bool: 

1231 self.to.reveal(self.role) 

1232 return True 

1233 

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 

1239 

1240 

1241class Pindian(UserAction): 

1242 no_reveal = True 

1243 card_usage = 'pindian' 

1244 

1245 def __init__(self, source, target): 

1246 self.source = source 

1247 self.target = target 

1248 

1249 def apply_action(self) -> bool: 

1250 src = self.source 

1251 tgt = self.target 

1252 g = self.game 

1253 

1254 pl = BatchList([tgt, src]) 

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

1256 

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

1264 

1265 pindian_card[p] = card 

1266 detach_cards([card]) 

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

1268 

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) 

1272 

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

1274 

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

1280 

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 

1288 

1289 

1290class Reforge(GenericAction): 

1291 card_usage = 'reforge' 

1292 

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

1294 self.source = source 

1295 self.target = target 

1296 self.card = card 

1297 

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

1302 

1303 return True 

1304 

1305 def is_valid(self): 

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

1307 

1308 

1309@register_eh 

1310class DyingHandler(THBEventHandler): 

1311 interested = ['action_after'] 

1312 

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 

1316 

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 

1324 

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 

1332 

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

1334 

1335 return act 

1336 

1337 

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) 

1343 

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 

1347 

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 

1359 

1360 

1361class ActionLimitExceeded(ActionShootdown): 

1362 pass 

1363 

1364 

1365class VitalityLimitExceeded(ActionLimitExceeded): 

1366 pass