# Coverage for thb/cards/spellcard.py : 93%

## 367 statements 355 run 12 missing 0 excluded 20 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

3# -- stdlib --

4 from typing import Sequence

5

6# -- third party --

7# -- own --

8 from game.base import InputTransaction, sync_primitive

9 from thb.actions import ActionStage, Damage, DrawCardStage, DrawCards, DropCards, FatetellAction

10 from thb.actions import ForEach, LaunchCard, PlayerTurn, UserAction, ask_for_action, detach_cards

11 from thb.actions import migrate_cards, random_choose_card, register_eh, user_choose_cards

12 from thb.cards import basic

13 from thb.cards.base import Card

14 from thb.inputlets import ChooseIndividualCardInputlet, ChoosePeerCardInputlet

15 from thb.mode import THBEventHandler

16 from utils.check import CheckFailed, check

17 from utils.misc import BatchList, flatten

18

19

20# -- code --

21 class SpellCardAction(UserAction):

22 non_responsive = False

23

24

25 class InstantSpellCardAction(SpellCardAction):

26 pass

27

28

29 class Demolition(InstantSpellCardAction):

30 # 城管执法

31

32 def apply_action(self):

33 g = self.game

34 src, tgt = self.source, self.target

35

36 if not (tgt.cards or tgt.showncards or tgt.equips or tgt.fatetell): return False 36 ↛ exit line 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 ↛ 43 line 42 didn't jump to line 43, because the condition on line 42 was never true

43 return False

44

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

51

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)

56

57

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

64

65 def apply_action(self):

66 if not isinstance(self.target_act, SpellCardAction): 66 ↛ 67 line 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

70

71

72@register_eh

73 class RejectHandler(THBEventHandler):

74 interested = ['action_before']

75 card_usage = 'launch'

76

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 ↛ exit line 79 didn't return from function 'handle', because the return on line 79 wasn't executed

80 if act.non_responsive: 80 ↛ 81 line 80 didn't jump to line 81, because the condition on line 80 was never true

81 return act

82

83 g = self.game

84

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

93

94 # if has_reject: break

95

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

101

102 break

103

104 has_reject = sync_primitive(has_reject, g.players)

105 if not has_reject: return act

106

107 self.target_act = act

108

109 pl = BatchList(p for p in g.players if not p.dead)

110

111 with InputTransaction('AskForRejectAction', pl) as trans:

112 p, rst = ask_for_action(self, pl, ('cards', 'showncards'), [], trans=trans)

113

114 if not p: return act

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

119

120 return act

121

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

130

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

134

135

136 class DelayedSpellCardAction(SpellCardAction): pass # 延时SC

137

138

139 class DelayedLaunchCard(UserAction):

140 def apply_action(self):

141 card = self.associated_card

142 action = card.delayed_action

143 assert issubclass(action, DelayedSpellCardAction)

144

145 t = self.target

146 migrate_cards([card], t.fatetell)

147

148 return True

149

150 def is_valid(self):

151 if not self.associated_card: return False 151 ↛ exit line 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 ↛ exit line 152 didn't return from function 'is_valid', because the return on line 152 wasn't executed

153 return not self.target.dead

154

155

156 class SealingArray(DelayedSpellCardAction, FatetellAction):

157 # 封魔阵

158 def __init__(self, source, target):

159 self.source = source

160 self.target = target

161 self.fatetell_target = target

162

163 from thb.cards.base import Card

164 self.fatetell_cond = lambda card: card.suit != Card.HEART

165

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

175

176 return True

177

178 return False

179

180 def fatetell_postprocess(self):

181 g = self.game

182 tgt = self.target

183 g.process_action(DropCards(None, tgt, [self.associated_card]))

184

185

186 class NazrinRod(InstantSpellCardAction):

187 # 纳兹琳的探宝棒

188

189 def apply_action(self):

190 g = self.game

191 g.process_action(DrawCards(self.target, amount=2))

192 return True

193

194

195 class SinsackDamage(Damage):

196 pass

197

198

199 class Sinsack(DelayedSpellCardAction, FatetellAction):

200 # 罪袋

201 def __init__(self, source, target):

202 self.source = source

203 self.target = target

204 self.fatetell_target = target

205

206 from thb.cards.base import Card

207 self.fatetell_cond = lambda c: c.suit == Card.SPADE and 1 <= c.number <= 8

208

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

214

215 return False

216

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

234

235

236 class YukariDimension(InstantSpellCardAction):

237 # 紫的隙间

238

239 def apply_action(self):

240 src = self.source

241 tgt = self.target

242 g = self.game

243

244 if not (tgt.cards or tgt.showncards or tgt.equips or tgt.fatetell): return False 244 ↛ exit line 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 ↛ 251 line 250 didn't jump to line 251, because the condition on line 250 was never true

251 return False

252

253 self.card = card

254 src.reveal(card)

255 migrate_cards([card], src.cards, unwrap=True)

256 return True

257

258 def is_valid(self):

259 if self.source.dead: 259 ↛ 260 line 259 didn't jump to line 260, because the condition on line 259 was never true

260 return False

261

262 tgt = self.target

263 catnames = ['cards', 'showncards', 'equips', 'fatetell']

264 return not tgt.dead and any(getattr(tgt, i) for i in catnames)

265

266

267 class BaseDuel(UserAction):

268 # 弹幕战

269 def __init__(self, source, target):

270 self.source = source

271 self.target = target

272

273 def apply_action(self):

274 g = self.game

275 source = self.source

276 target = self.target

277

278 s, t = source, target

279 while True:

280 if t.dead: break 280 ↛ 284 line 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

283

284 if not t.dead: 284 ↛ 287 line 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))

286

287 self.winner = s

288

289 return True

290

291 def is_valid(self):

292 return not self.target.dead

293

294

295 class Duel(BaseDuel, InstantSpellCardAction):

296 pass

297

298

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

310

311 def is_valid(self):

312 return not self.target.dead

313

314

315 class MapCannon(ForEach):

316 action_cls = MapCannonEffect

317 group_effect = True

318

319

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

330

331 def is_valid(self):

332 return not self.target.dead

333

334

335 class DemonParade(ForEach):

336 action_cls = DemonParadeEffect

337 group_effect = True

338

339

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

350

351 def is_valid(self):

352 return not self.target.dead

353

354

355 class Feast(ForEach):

356 action_cls = FeastEffect

357 group_effect = True

358

359

360 class 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 ↛ exit line 367 didn't return from function 'apply_action', because the return on line 367 wasn't executed

368 tgt = self.target

369

370 card = g.user_input(

371 [tgt],

372 ChooseIndividualCardInputlet(self, cards_avail),

373 trans=pact.trans,

374 ) or random_choose_card(g, [cards_avail])

375

376 migrate_cards([card], tgt.cards)

377

378 pact.trans.notify('harvest_choose', card)

379 self.card = card

380 return True

381

382 def is_valid(self):

383 try:

384 cards = ForEach.get_actual_action(self).cards

385 except Exception:

386 return False

387

388 if self.target.dead: 388 ↛ 389 line 388 didn't jump to line 389, because the condition on line 388 was never true

389 return False

390

391 return bool([c for c in cards if c.detached])

392

393

394 class Harvest(ForEach):

395 action_cls = HarvestEffect

396 group_effect = True

397

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

408

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)

414

415

416 class DollControl(InstantSpellCardAction):

417 card_usage = 'launch'

418

419 def apply_action(self):

420 tl = self.target_list

421 assert len(tl) == 2

422 src = self.source

423

424 attacker, victim = tl

425 cards = user_choose_cards(self, attacker, ['cards', 'showncards'])

426 g = self.game

427

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

435

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)

441

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

445

446 def is_valid(self):

447 if self.target.dead: 447 ↛ 448 line 447 didn't jump to line 448, because the condition on line 447 was never true

448 return False

449

450 assert len(self.target_list) == 2

451

452 attacker, victim = self.target_list

453

454 if not any(e.equipment_category == 'weapon' for e in attacker.equips):

455 return False

456

457 from .definition import AttackCard

458 if not LaunchCard(attacker, [victim], AttackCard()).can_fire():

459 return False

460

461 return True

462

463

464 class DonationBoxEffect(InstantSpellCardAction):

465 card_usage = 'handover'

466

467 def apply_action(self):

468 t = self.target

469 src = self.source

470 g = self.game

471

472 if not (t.cards or t.showncards or t.equips): return False 472 ↛ exit line 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:

477 cards = [random_choose_card(g, cats)]

478

479 if cards: 479 ↛ 483 line 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)

482

483 return True

484

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)

490

491 def is_valid(self):

492 t = self.target

493 if t.dead or self.source.dead: return False 493 ↛ exit line 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

495 return False

496

497

498 class DonationBox(ForEach):

499 action_cls = DonationBoxEffect

500

501 def is_valid(self):

502 tl = self.target_list

503 if not 0 < len(tl) <= 2: return False 503 ↛ exit line 503 didn't return from function 'is_valid', because the return on line 503 wasn't executed

504

505 if not all(

506 t.cards or t.showncards or t.equips

507 for t in tl

508 ): return False

509

510 return True

511

512

513 class FrozenFrog(DelayedSpellCardAction, FatetellAction):

514 # 冻青蛙

515 def __init__(self, source, target):

516 self.source = source

517 self.target = target

518 self.fatetell_target = target

519

520 self.fatetell_cond = lambda c: c.suit != Card.SPADE

521

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

531

532 return True

533

534 return False

535

536 def fatetell_postprocess(self):

537 g = self.game

538 tgt = self.target

539 g.process_action(DropCards(None, tgt, [self.associated_card]))