Coverage for thb/cards/spellcard.py : 91%
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 -*-
3# -- stdlib --
4from typing import Sequence
6# -- third party --
7# -- own --
8from game.base import InputTransaction, sync_primitive
9from thb.actions import ActionStage, Damage, DrawCardStage, DrawCards, DropCards, FatetellAction
10from thb.actions import ForEach, LaunchCard, PlayerTurn, UserAction, ask_for_action, detach_cards
11from thb.actions import migrate_cards, random_choose_card, register_eh, user_choose_cards
12from thb.cards import basic
13from thb.cards.base import Card
14from thb.inputlets import ChooseIndividualCardInputlet, ChoosePeerCardInputlet
15from thb.mode import THBEventHandler
16from utils.check import CheckFailed, check
17from utils.misc import BatchList, flatten
20# -- code --
21class SpellCardAction(UserAction):
22 non_responsive = False
25class InstantSpellCardAction(SpellCardAction):
26 pass
29class Demolition(InstantSpellCardAction):
30 # 城管执法
32 def apply_action(self):
33 g = self.game
34 src, tgt = self.source, self.target
36 if not (tgt.cards or tgt.showncards or tgt.equips or tgt.fatetell): return False 36 ↛ exitline 36 didn't return from function 'apply_action', because the return on line 36 wasn't executed
37 catnames = ('cards', 'showncards', 'equips', 'fatetell')
38 cats = [getattr(tgt, i) for i in catnames]
39 card = g.user_input([src], ChoosePeerCardInputlet(self, tgt, catnames))
40 if not card:
41 card = random_choose_card(g, cats)
42 if not card: 42 ↛ 43line 42 didn't jump to line 43, because the condition on line 42 was never true
43 return False
45 self.card = card
46 g.players.exclude(tgt).reveal(card)
47 g.process_action(
48 DropCards(src, tgt, cards=[card])
49 )
50 return True
52 def is_valid(self):
53 tgt = self.target
54 catnames = ('cards', 'showncards', 'equips', 'fatetell')
55 return not tgt.dead and any(getattr(tgt, i) for i in catnames)
58class Reject(InstantSpellCardAction):
59 # 好人卡
60 def __init__(self, source, target_act):
61 self.source = source
62 self.target_act = target_act
63 self.target = target_act.target
65 def apply_action(self):
66 if not isinstance(self.target_act, SpellCardAction): 66 ↛ 67line 66 didn't jump to line 67, because the condition on line 66 was never true
67 return False
68 self.target_act.cancelled = True
69 return True
72@register_eh
73class RejectHandler(THBEventHandler):
74 interested = ['action_before']
75 card_usage = 'launch'
77 def handle(self, evt_type, act):
78 if evt_type == 'action_before' and isinstance(act, SpellCardAction):
79 if act.cancelled: return act # some other thing have done the job 79 ↛ exitline 79 didn't return from function 'handle', because the return on line 79 wasn't executed
80 if act.non_responsive: 80 ↛ 81line 80 didn't jump to line 81, because the condition on line 80 was never true
81 return act
83 g = self.game
85 has_reject = False
86 while g.is_server_side():
87 # from thb.characters.base import Character
88 # from thb.characters.reimu import SpiritualAttack
89 # for p in g.players:
90 # if isinstance(p, Character) and p.has_skill(SpiritualAttack):
91 # has_reject = True
92 # break
94 # if has_reject: break
96 from thb.cards.definition import RejectCard
97 for c in flatten([[p.cards, p.showncards] for p in g.players]):
98 if isinstance(c, RejectCard):
99 has_reject = True
100 break
102 break
104 has_reject = sync_primitive(has_reject, g.players)
105 if not has_reject: return act
107 self.target_act = act
109 pl = BatchList(p for p in g.players if not p.dead)
111 with InputTransaction('AskForRejectAction', pl) as trans:
112 p, rst = ask_for_action(self, pl, ('cards', 'showncards'), [], trans=trans)
114 if not p: return act 114 ↛ 115line 114 didn't jump to line 115, because the condition on line 114 was never false
115 assert rst
116 cards, _ = rst
117 assert cards and self.cond(cards)
118 g.process_action(LaunchCard(p, [act.target], cards[0], Reject(p, act)))
120 return act
122 def cond(self, cards: Sequence[Card]) -> bool:
123 from thb.cards.definition import RejectCard
124 try:
125 check(len(cards) == 1)
126 check(cards[0].is_card(RejectCard))
127 return True
128 except CheckFailed:
129 return False
131 def ask_for_action_verify(self, p, cl, tl):
132 act = self.target_act
133 return LaunchCard(p, [act.target], cl[0], Reject(p, act)).can_fire()
136class DelayedSpellCardAction(SpellCardAction): pass # 延时SC
139class DelayedLaunchCard(UserAction):
140 def apply_action(self):
141 card = self.associated_card
142 action = card.delayed_action
143 assert issubclass(action, DelayedSpellCardAction)
145 t = self.target
146 migrate_cards([card], t.fatetell)
148 return True
150 def is_valid(self):
151 if not self.associated_card: return False 151 ↛ exitline 151 didn't return from function 'is_valid', because the return on line 151 wasn't executed
152 if not len(self.target_list) == 1: return False 152 ↛ exitline 152 didn't return from function 'is_valid', because the return on line 152 wasn't executed
153 return not self.target.dead
156class SealingArray(DelayedSpellCardAction, FatetellAction):
157 # 封魔阵
158 def __init__(self, source, target):
159 self.source = source
160 self.target = target
161 self.fatetell_target = target
163 from thb.cards.base import Card
164 self.fatetell_cond = lambda card: card.suit != Card.HEART
166 def fatetell_action(self, ft):
167 g = self.game
168 if ft.succeeded:
169 turn = PlayerTurn.get_current(g)
170 assert turn.target is self.target, (turn.target, self.target)
171 try:
172 turn.pending_stages.remove(ActionStage)
173 except Exception:
174 pass
176 return True
178 return False
180 def fatetell_postprocess(self):
181 g = self.game
182 tgt = self.target
183 g.process_action(DropCards(None, tgt, [self.associated_card]))
186class NazrinRod(InstantSpellCardAction):
187 # 纳兹琳的探宝棒
189 def apply_action(self):
190 g = self.game
191 g.process_action(DrawCards(self.target, amount=2))
192 return True
195class SinsackDamage(Damage):
196 pass
199class Sinsack(DelayedSpellCardAction, FatetellAction):
200 # 罪袋
201 def __init__(self, source, target):
202 self.source = source
203 self.target = target
204 self.fatetell_target = target
206 from thb.cards.base import Card
207 self.fatetell_cond = lambda c: c.suit == Card.SPADE and 1 <= c.number <= 8
209 def fatetell_action(self, ft):
210 if ft.succeeded:
211 g = self.game
212 g.process_action(SinsackDamage(None, self.target, amount=3))
213 return True
215 return False
217 def fatetell_postprocess(self):
218 g = self.game
219 tgt = self.target
220 if not self.cancelled and self.succeeded:
221 g.process_action(DropCards(None, tgt, [self.associated_card]))
222 else:
223 pl = g.players
224 stop = pl.index(tgt)
225 next = stop - len(pl) + 1
226 while next < stop: 226 ↛ 231line 226 didn't jump to line 231, because the condition on line 226 was never false
227 if not pl[next].dead:
228 migrate_cards([self.associated_card], pl[next].fatetell)
229 return
230 next += 1
231 if next == stop and len(pl) == 2:
232 migrate_cards([self.associated_card], pl[next-1].fatetell)
233 return
236class YukariDimension(InstantSpellCardAction):
237 # 紫的隙间
239 def apply_action(self):
240 src = self.source
241 tgt = self.target
242 g = self.game
244 if not (tgt.cards or tgt.showncards or tgt.equips or tgt.fatetell): return False 244 ↛ exitline 244 didn't return from function 'apply_action', because the return on line 244 wasn't executed
245 catnames = ('cards', 'showncards', 'equips', 'fatetell')
246 cats = [getattr(tgt, i) for i in catnames]
247 card = g.user_input([src], ChoosePeerCardInputlet(self, tgt, catnames))
248 if not card:
249 card = random_choose_card(g, cats)
250 if not card: 250 ↛ 251line 250 didn't jump to line 251, because the condition on line 250 was never true
251 return False
253 self.card = card
254 src.reveal(card)
255 migrate_cards([card], src.cards, unwrap=True)
256 return True
258 def is_valid(self):
259 if self.source.dead: 259 ↛ 260line 259 didn't jump to line 260, because the condition on line 259 was never true
260 return False
262 tgt = self.target
263 catnames = ['cards', 'showncards', 'equips', 'fatetell']
264 return not tgt.dead and any(getattr(tgt, i) for i in catnames)
267class BaseDuel(UserAction):
268 # 弹幕战
269 def __init__(self, source, target):
270 self.source = source
271 self.target = target
273 def apply_action(self):
274 g = self.game
275 source = self.source
276 target = self.target
278 s, t = source, target
279 while True:
280 if t.dead: break 280 ↛ 284line 280 didn't jump to line 284, because the break on line 280 wasn't executed
281 if not g.process_action(basic.UseAttack(t)): break
282 s, t = t, s
284 if not t.dead: 284 ↛ 287line 284 didn't jump to line 287, because the condition on line 284 was never false
285 g.process_action(Damage(s, t, amount=1))
287 self.winner = s
289 return True
291 def is_valid(self):
292 return not self.target.dead
295class Duel(BaseDuel, InstantSpellCardAction):
296 pass
299class MapCannonEffect(InstantSpellCardAction):
300 # 地图炮
301 def apply_action(self):
302 g = self.game
303 source, target = self.source, self.target
304 graze_action = basic.UseGraze(target)
305 if not g.process_action(graze_action):
306 g.process_action(Damage(source, target, amount=1))
307 return True
308 else:
309 return False
311 def is_valid(self):
312 return not self.target.dead
315class MapCannon(ForEach):
316 action_cls = MapCannonEffect
317 group_effect = True
320class DemonParadeEffect(InstantSpellCardAction):
321 def apply_action(self):
322 g = self.game
323 source, target = self.source, self.target
324 use_action = basic.UseAttack(target)
325 if not g.process_action(use_action):
326 g.process_action(Damage(source, target, amount=1))
327 return True
328 else:
329 return False
331 def is_valid(self):
332 return not self.target.dead
335class DemonParade(ForEach):
336 action_cls = DemonParadeEffect
337 group_effect = True
340class FeastEffect(InstantSpellCardAction):
341 # 宴会
342 def apply_action(self):
343 src, tgt = self.source, self.target
344 g = self.game
345 if tgt.life < tgt.maxlife:
346 g.process_action(basic.Heal(src, tgt))
347 else:
348 g.process_action(basic.Wine(src, tgt))
349 return True
351 def is_valid(self):
352 return not self.target.dead
355class Feast(ForEach):
356 action_cls = FeastEffect
357 group_effect = True
360class HarvestEffect(InstantSpellCardAction):
361 # 五谷丰登 效果
362 def apply_action(self):
363 g = self.game
364 pact = ForEach.get_actual_action(self)
365 cards = pact.cards
366 cards_avail = [c for c in cards if c.detached]
367 if not cards_avail: return False 367 ↛ exitline 367 didn't return from function 'apply_action', because the return on line 367 wasn't executed
368 tgt = self.target
370 card = g.user_input(
371 [tgt],
372 ChooseIndividualCardInputlet(self, cards_avail),
373 trans=pact.trans,
374 ) or random_choose_card(g, [cards_avail])
376 migrate_cards([card], tgt.cards)
378 pact.trans.notify('harvest_choose', card)
379 self.card = card
380 return True
382 def is_valid(self):
383 try:
384 cards = ForEach.get_actual_action(self).cards
385 except Exception:
386 return False
388 if self.target.dead: 388 ↛ 389line 388 didn't jump to line 389, because the condition on line 388 was never true
389 return False
391 return bool([c for c in cards if c.detached])
394class Harvest(ForEach):
395 action_cls = HarvestEffect
396 group_effect = True
398 def prepare(self):
399 tl = self.target_list
400 g = self.game
401 cards = g.deck.getcards(len(tl))
402 g.players.reveal(cards)
403 detach_cards(cards)
404 trans = InputTransaction('HarvestChoose', g.players, cards=cards)
405 trans.begin()
406 self.cards = cards
407 self.trans = trans
409 def cleanup(self):
410 g = self.game
411 self.trans.end()
412 g.emit_event('harvest_finish', self)
413 migrate_cards([c for c in self.cards if c.detached], g.deck.droppedcards)
416class DollControl(InstantSpellCardAction):
417 card_usage = 'launch'
419 def apply_action(self):
420 tl = self.target_list
421 assert len(tl) == 2
422 src = self.source
424 attacker, victim = tl
425 cards = user_choose_cards(self, attacker, ['cards', 'showncards'])
426 g = self.game
428 if cards:
429 g.players.reveal(cards)
430 g.process_action(LaunchCard(attacker, [victim], cards[0]))
431 else:
432 l = [e for e in attacker.equips if e.equipment_category == 'weapon']
433 migrate_cards(l, src.cards)
434 return True
436 def cond(self, cl):
437 if len(cl) != 1: return False
438 c = cl[0]
439 if not c.associated_action: return False
440 return issubclass(c.associated_action, basic.Attack)
442 def ask_for_action_verify(self, p, cl, tl):
443 attacker, victim = self.target_list
444 return LaunchCard(attacker, [victim], cl[0]).can_fire()
446 def is_valid(self):
447 if self.target.dead: 447 ↛ 448line 447 didn't jump to line 448, because the condition on line 447 was never true
448 return False
450 assert len(self.target_list) == 2
452 attacker, victim = self.target_list
454 if not any(e.equipment_category == 'weapon' for e in attacker.equips):
455 return False
457 from .definition import AttackCard
458 if not LaunchCard(attacker, [victim], AttackCard()).can_fire():
459 return False
461 return True
464class DonationBoxEffect(InstantSpellCardAction):
465 card_usage = 'handover'
467 def apply_action(self):
468 t = self.target
469 src = self.source
470 g = self.game
472 if not (t.cards or t.showncards or t.equips): return False 472 ↛ exitline 472 didn't return from function 'apply_action', because the return on line 472 wasn't executed
473 catnames = ('cards', 'showncards', 'equips')
474 cats = [getattr(t, i) for i in catnames]
475 cards = user_choose_cards(self, t, catnames)
476 if not cards: 476 ↛ 479line 476 didn't jump to line 479, because the condition on line 476 was never false
477 cards = [random_choose_card(g, cats)]
479 if cards: 479 ↛ 483line 479 didn't jump to line 483, because the condition on line 479 was never false
480 g.players.exclude(t).reveal(cards)
481 migrate_cards(cards, src.showncards)
483 return True
485 def cond(self, cards):
486 from .base import Skill
487 return len(cards) == 1 and cards[0].resides_in.type in (
488 'cards', 'showncards', 'equips'
489 ) and not cards[0].is_card(Skill)
491 def is_valid(self):
492 t = self.target
493 if t.dead or self.source.dead: return False 493 ↛ exitline 493 didn't return from function 'is_valid', because the return on line 493 wasn't executed
494 if t.cards or t.showncards or t.equips: return True 494 ↛ 495line 494 didn't jump to line 495, because the condition on line 494 was never false
495 return False
498class DonationBox(ForEach):
499 action_cls = DonationBoxEffect
501 def is_valid(self):
502 tl = self.target_list
503 if not 0 < len(tl) <= 2: return False 503 ↛ exitline 503 didn't return from function 'is_valid', because the return on line 503 wasn't executed
505 if not all(
506 t.cards or t.showncards or t.equips
507 for t in tl
508 ): return False
510 return True
513class FrozenFrog(DelayedSpellCardAction, FatetellAction):
514 # 冻青蛙
515 def __init__(self, source, target):
516 self.source = source
517 self.target = target
518 self.fatetell_target = target
520 self.fatetell_cond = lambda c: c.suit != Card.SPADE
522 def fatetell_action(self, ft):
523 g = self.game
524 if ft.succeeded:
525 turn = PlayerTurn.get_current(g)
526 assert turn.target is self.target, (turn.target, self.target)
527 try:
528 turn.pending_stages.remove(DrawCardStage)
529 except Exception:
530 pass
532 return True
534 return False
536 def fatetell_postprocess(self):
537 g = self.game
538 tgt = self.target
539 g.process_action(DropCards(None, tgt, [self.associated_card]))