Coverage for thb/cards/spellcard.py : 22%
Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# -*- coding: utf-8 -*-
2from __future__ import annotations
4# -- stdlib --
5from typing import Sequence
7# -- third party --
8# -- own --
9from game.base import InputTransaction, sync_primitive
10from thb.actions import ActionStage, Damage, DrawCardStage, DrawCards, DropCards, FatetellAction
11from thb.actions import ForEach, LaunchCard, PlayerTurn, UserAction, ask_for_action, detach_cards
12from thb.actions import migrate_cards, random_choose_card, register_eh, user_choose_cards
13from thb.cards import basic
14from thb.cards.base import Card
15from thb.inputlets import ChooseIndividualCardInputlet, ChoosePeerCardInputlet
16from thb.mode import THBEventHandler
17from utils.check import CheckFailed, check
18from utils.misc import BatchList, flatten
21# -- code --
22class SpellCardAction(UserAction):
23 non_responsive = False
26class InstantSpellCardAction(SpellCardAction):
27 pass
30class Demolition(InstantSpellCardAction):
31 # 城管执法
33 def apply_action(self):
34 g = self.game
35 src, tgt = self.source, self.target
37 if not (tgt.cards or tgt.showncards or tgt.equips or tgt.fatetell): return False
38 catnames = ('cards', 'showncards', 'equips', 'fatetell')
39 cats = [getattr(tgt, i) for i in catnames]
40 card = g.user_input([src], ChoosePeerCardInputlet(self, tgt, catnames))
41 if not card:
42 card = random_choose_card(g, cats)
43 if not card:
44 return False
46 self.card = card
47 g.players.exclude(tgt).reveal(card)
48 g.process_action(
49 DropCards(src, tgt, cards=[card])
50 )
51 return True
53 def is_valid(self):
54 tgt = self.target
55 catnames = ('cards', 'showncards', 'equips', 'fatetell')
56 return not tgt.dead and any(getattr(tgt, i) for i in catnames)
59class Reject(InstantSpellCardAction):
60 # 好人卡
61 def __init__(self, source, target_act):
62 self.source = source
63 self.target_act = target_act
64 self.target = target_act.target
66 def apply_action(self):
67 if not isinstance(self.target_act, SpellCardAction):
68 return False
69 self.target_act.cancelled = True
70 return True
73@register_eh
74class RejectHandler(THBEventHandler):
75 interested = ['action_before']
76 card_usage = 'launch'
78 def handle(self, evt_type, act):
79 if evt_type == 'action_before' and isinstance(act, SpellCardAction): 79 ↛ 80line 79 didn't jump to line 80, because the condition on line 79 was never true
80 if act.cancelled: return act # some other thing have done the job
81 if act.non_responsive:
82 return act
84 g = self.game
86 has_reject = False
87 while g.is_server_side():
88 # from thb.characters.base import Character
89 # from thb.characters.reimu import SpiritualAttack
90 # for p in g.players:
91 # if isinstance(p, Character) and p.has_skill(SpiritualAttack):
92 # has_reject = True
93 # break
95 # if has_reject: break
97 from thb.cards.definition import RejectCard
98 for c in flatten([[p.cards, p.showncards] for p in g.players]):
99 if isinstance(c, RejectCard):
100 has_reject = True
101 break
103 break
105 has_reject = sync_primitive(has_reject, g.players.player)
106 if not has_reject: return act
108 self.target_act = act
110 pl = BatchList(p for p in g.players if not p.dead)
112 with InputTransaction('AskForRejectAction', pl) as trans:
113 p, rst = ask_for_action(self, pl, ('cards', 'showncards'), [], trans=trans)
115 if not p: return act
116 assert rst
117 cards, _ = rst
118 assert cards and self.cond(cards)
119 g.process_action(LaunchCard(p, [act.target], cards[0], Reject(p, act)))
121 return act
123 def cond(self, cards: Sequence[Card]) -> bool:
124 from thb.cards.definition import RejectCard
125 try:
126 check(len(cards) == 1)
127 check(cards[0].is_card(RejectCard))
128 return True
129 except CheckFailed:
130 return False
132 def ask_for_action_verify(self, p, cl, tl):
133 act = self.target_act
134 return LaunchCard(p, [act.target], cl[0], Reject(p, act)).can_fire()
137class DelayedSpellCardAction(SpellCardAction):
138 pass
141class DelayedLaunchCard(UserAction):
142 def apply_action(self):
143 card = self.associated_card
144 t = self.target
145 migrate_cards([card], t.fatetell)
147 return True
149 def is_valid(self):
150 if not self.associated_card: return False
151 if not len(self.target_list) == 1: return False
152 return not self.target.dead
155class SealingArray(DelayedSpellCardAction, FatetellAction):
156 # 封魔阵
157 def __init__(self, source, target):
158 self.source = source
159 self.target = target
160 self.fatetell_target = target
162 def fatetell_action(self, ft):
163 g = self.game
164 if ft.succeeded:
165 turn = PlayerTurn.get_current(g)
166 assert turn.target is self.target, (turn.target, self.target)
167 try:
168 turn.pending_stages.remove(ActionStage)
169 except Exception:
170 pass
172 return True
174 return False
176 def fatetell_cond(self, c: Card):
177 return c.suit != Card.HEART
179 def fatetell_postprocess(self):
180 g = self.game
181 tgt = self.target
182 g.process_action(DropCards(None, tgt, [self.associated_card]))
185class NazrinRod(InstantSpellCardAction):
186 # 纳兹琳的探宝棒
188 def apply_action(self):
189 g = self.game
190 g.process_action(DrawCards(self.target, amount=2))
191 return True
194class SinsackDamage(Damage):
195 pass
198class Sinsack(DelayedSpellCardAction, FatetellAction):
199 # 罪袋
200 def __init__(self, source, target):
201 self.source = source
202 self.target = target
203 self.fatetell_target = target
205 def fatetell_action(self, ft):
206 if ft.succeeded:
207 g = self.game
208 g.process_action(SinsackDamage(None, self.target, amount=3))
209 return True
211 return False
213 def fatetell_cond(self, c: Card):
214 return c.suit == Card.SPADE and 1 <= c.number <= 8
216 def fatetell_postprocess(self):
217 g = self.game
218 tgt = self.target
219 if not self.cancelled and self.succeeded:
220 g.process_action(DropCards(None, tgt, [self.associated_card]))
221 else:
222 pl = g.players
223 stop = pl.index(tgt)
224 next = stop - len(pl) + 1
225 while next < stop:
226 if not pl[next].dead:
227 migrate_cards([self.associated_card], pl[next].fatetell)
228 return
229 next += 1
230 if next == stop and len(pl) == 2:
231 migrate_cards([self.associated_card], pl[next-1].fatetell)
232 return
235class YukariDimension(InstantSpellCardAction):
236 # 紫的隙间
238 def apply_action(self):
239 src = self.source
240 tgt = self.target
241 g = self.game
243 if not (tgt.cards or tgt.showncards or tgt.equips or tgt.fatetell): return False
244 catnames = ('cards', 'showncards', 'equips', 'fatetell')
245 cats = [getattr(tgt, i) for i in catnames]
246 card = g.user_input([src], ChoosePeerCardInputlet(self, tgt, catnames))
247 if not card:
248 card = random_choose_card(g, cats)
249 if not card:
250 return False
252 self.card = card
253 src.reveal(card)
254 migrate_cards([card], src.cards, unwrap=True)
255 return True
257 def is_valid(self):
258 if self.source.dead:
259 return False
261 tgt = self.target
262 catnames = ['cards', 'showncards', 'equips', 'fatetell']
263 return not tgt.dead and any(getattr(tgt, i) for i in catnames)
266class BaseDuel(UserAction):
267 # 弹幕战
268 def __init__(self, source, target):
269 self.source = source
270 self.target = target
272 def apply_action(self):
273 g = self.game
274 source = self.source
275 target = self.target
277 s, t = source, target
278 while True:
279 if t.dead: break
280 if not g.process_action(basic.UseAttack(t)): break
281 s, t = t, s
283 if not t.dead:
284 g.process_action(Damage(s, t, amount=1))
286 self.winner = s
288 return True
290 def is_valid(self):
291 return not self.target.dead
294class Duel(BaseDuel, InstantSpellCardAction):
295 pass
298class MapCannonEffect(InstantSpellCardAction):
299 # 地图炮
300 def apply_action(self):
301 g = self.game
302 source, target = self.source, self.target
303 graze_action = basic.UseGraze(target)
304 if not g.process_action(graze_action):
305 g.process_action(Damage(source, target, amount=1))
306 return True
307 else:
308 return False
310 def is_valid(self):
311 return not self.target.dead
314class MapCannon(ForEach):
315 action_cls = MapCannonEffect
316 group_effect = True
319class DemonParadeEffect(InstantSpellCardAction):
320 def apply_action(self):
321 g = self.game
322 source, target = self.source, self.target
323 use_action = basic.UseAttack(target)
324 if not g.process_action(use_action):
325 g.process_action(Damage(source, target, amount=1))
326 return True
327 else:
328 return False
330 def is_valid(self):
331 return not self.target.dead
334class DemonParade(ForEach):
335 action_cls = DemonParadeEffect
336 group_effect = True
339class FeastEffect(InstantSpellCardAction):
340 # 宴会
341 def apply_action(self):
342 src, tgt = self.source, self.target
343 g = self.game
344 if tgt.life < tgt.maxlife:
345 g.process_action(basic.Heal(src, tgt))
346 else:
347 g.process_action(basic.Wine(src, tgt))
348 return True
350 def is_valid(self):
351 return not self.target.dead
354class Feast(ForEach):
355 action_cls = FeastEffect
356 group_effect = True
359class HarvestEffect(InstantSpellCardAction):
360 # 五谷丰登 效果
361 def apply_action(self):
362 g = self.game
363 pact = ForEach.get_actual_action(self)
364 cards = pact.cards
365 cards_avail = [c for c in cards if c.detached]
366 if not cards_avail: return False
367 tgt = self.target
369 card = g.user_input(
370 [tgt],
371 ChooseIndividualCardInputlet(self, cards_avail),
372 trans=pact.trans,
373 ) or random_choose_card(g, [cards_avail])
375 migrate_cards([card], tgt.cards)
377 pact.trans.notify('harvest_choose', card)
378 self.card = card
379 return True
381 def is_valid(self):
382 try:
383 cards = ForEach.get_actual_action(self).cards
384 except Exception:
385 return False
387 if self.target.dead:
388 return False
390 return bool([c for c in cards if c.detached])
393class Harvest(ForEach):
394 action_cls = HarvestEffect
395 group_effect = True
397 def prepare(self):
398 tl = self.target_list
399 g = self.game
400 cards = g.deck.getcards(len(tl))
401 g.players.reveal(cards)
402 detach_cards(cards)
403 trans = InputTransaction('HarvestChoose', g.players, cards=cards)
404 trans.begin()
405 self.cards = cards
406 self.trans = trans
408 def cleanup(self):
409 g = self.game
410 self.trans.end()
411 g.emit_event('harvest_finish', self)
412 migrate_cards([c for c in self.cards if c.detached], g.deck.droppedcards)
415class DollControl(InstantSpellCardAction):
416 card_usage = 'launch'
418 def apply_action(self):
419 tl = self.target_list
420 assert len(tl) == 2
421 src = self.source
423 attacker, victim = tl
424 cards = user_choose_cards(self, attacker, ['cards', 'showncards'])
425 g = self.game
427 if cards:
428 g.players.reveal(cards)
429 g.process_action(LaunchCard(attacker, [victim], cards[0]))
430 else:
431 l = [e for e in attacker.equips if e.equipment_category == 'weapon']
432 migrate_cards(l, src.cards)
433 return True
435 def cond(self, cl):
436 if len(cl) != 1: return False
437 c = cl[0]
438 if not c.associated_action: return False
439 return issubclass(c.associated_action, basic.Attack)
441 def ask_for_action_verify(self, p, cl, tl):
442 attacker, victim = self.target_list
443 return LaunchCard(attacker, [victim], cl[0]).can_fire()
445 def is_valid(self):
446 if self.target.dead:
447 return False
449 assert len(self.target_list) == 2
451 attacker, victim = self.target_list
453 if not any(e.equipment_category == 'weapon' for e in attacker.equips):
454 return False
456 from .definition import AttackCard
457 if not LaunchCard(attacker, [victim], AttackCard()).can_fire():
458 return False
460 return True
463class DonationBoxEffect(InstantSpellCardAction):
464 card_usage = 'handover'
466 def apply_action(self):
467 t = self.target
468 src = self.source
469 g = self.game
471 if not (t.cards or t.showncards or t.equips): return False
472 catnames = ('cards', 'showncards', 'equips')
473 cats = [getattr(t, i) for i in catnames]
474 cards = user_choose_cards(self, t, catnames)
475 if not cards:
476 cards = [random_choose_card(g, cats)]
478 if cards:
479 g.players.exclude(t).reveal(cards)
480 migrate_cards(cards, src.showncards)
482 return True
484 def cond(self, cards):
485 from .base import Skill
486 return len(cards) == 1 and cards[0].resides_in.type in (
487 'cards', 'showncards', 'equips'
488 ) and not cards[0].is_card(Skill)
490 def is_valid(self):
491 t = self.target
492 if t.dead or self.source.dead: return False
493 if t.cards or t.showncards or t.equips: return True
494 return False
497class DonationBox(ForEach):
498 action_cls = DonationBoxEffect
500 def is_valid(self):
501 tl = self.target_list
502 if not 0 < len(tl) <= 2: return False
504 if not all(
505 t.cards or t.showncards or t.equips
506 for t in tl
507 ): return False
509 return True
512class FrozenFrog(DelayedSpellCardAction, FatetellAction):
513 # 冻青蛙
514 def __init__(self, source, target):
515 self.source = source
516 self.target = target
517 self.fatetell_target = target
519 def fatetell_action(self, ft):
520 g = self.game
521 if ft.succeeded:
522 turn = PlayerTurn.get_current(g)
523 assert turn.target is self.target, (turn.target, self.target)
524 try:
525 turn.pending_stages.remove(DrawCardStage)
526 except Exception:
527 pass
529 return True
531 return False
533 def fatetell_cond(self, c: Card):
534 return c.suit != Card.SPADE
536 def fatetell_postprocess(self):
537 g = self.game
538 tgt = self.target
539 g.process_action(DropCards(None, tgt, [self.associated_card]))