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

## 368 statements 104 run 264 missing 0 excluded 1 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 typing import Sequence

6

7# -- third party --

8# -- own --

9 from game.base import InputTransaction, sync_primitive

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

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

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

13 from thb.cards import basic

14 from thb.cards.base import Card

15 from thb.inputlets import ChooseIndividualCardInputlet, ChoosePeerCardInputlet

16 from thb.mode import THBEventHandler

17 from utils.check import CheckFailed, check

18 from utils.misc import BatchList, flatten

19

20

21# -- code --

22 class SpellCardAction(UserAction):

23 non_responsive = False

24

25

26 class InstantSpellCardAction(SpellCardAction):

27 pass

28

29

30 class Demolition(InstantSpellCardAction):

31 # 城管执法

32

33 def apply_action(self):

34 g = self.game

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

36

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

45

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

52

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)

57

58

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

65

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

71

72

73@register_eh

74 class RejectHandler(THBEventHandler):

75 interested = ['action_before']

76 card_usage = 'launch'

77

78 def handle(self, evt_type, act):

79 if evt_type == 'action_before' and isinstance(act, SpellCardAction): 79 ↛ 80 line 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

83

84 g = self.game

85

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

94

95 # if has_reject: break

96

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

102

103 break

104

105 has_reject = sync_primitive(has_reject, g.players.player)

106 if not has_reject: return act

107

108 self.target_act = act

109

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

111

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

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

114

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

120

121 return act

122

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

131

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

135

136

137 class DelayedSpellCardAction(SpellCardAction):

138 pass

139

140

141 class DelayedLaunchCard(UserAction):

142 def apply_action(self):

143 card = self.associated_card

144 t = self.target

145 migrate_cards([card], t.fatetell)

146

147 return True

148

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

153

154

155 class SealingArray(DelayedSpellCardAction, FatetellAction):

156 # 封魔阵

157 def __init__(self, source, target):

158 self.source = source

159 self.target = target

160 self.fatetell_target = target

161

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

171

172 return True

173

174 return False

175

176 def fatetell_cond(self, c: Card):

177 return c.suit != Card.HEART

178

179 def fatetell_postprocess(self):

180 g = self.game

181 tgt = self.target

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

183

184

185 class NazrinRod(InstantSpellCardAction):

186 # 纳兹琳的探宝棒

187

188 def apply_action(self):

189 g = self.game

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

191 return True

192

193

194 class SinsackDamage(Damage):

195 pass

196

197

198 class Sinsack(DelayedSpellCardAction, FatetellAction):

199 # 罪袋

200 def __init__(self, source, target):

201 self.source = source

202 self.target = target

203 self.fatetell_target = target

204

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

210

211 return False

212

213 def fatetell_cond(self, c: Card):

214 return c.suit == Card.SPADE and 1 <= c.number <= 8

215

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

233

234

235 class YukariDimension(InstantSpellCardAction):

236 # 紫的隙间

237

238 def apply_action(self):

239 src = self.source

240 tgt = self.target

241 g = self.game

242

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

251

252 self.card = card

253 src.reveal(card)

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

255 return True

256

257 def is_valid(self):

258 if self.source.dead:

259 return False

260

261 tgt = self.target

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

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

264

265

266 class BaseDuel(UserAction):

267 # 弹幕战

268 def __init__(self, source, target):

269 self.source = source

270 self.target = target

271

272 def apply_action(self):

273 g = self.game

274 source = self.source

275 target = self.target

276

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

282

283 if not t.dead:

284 g.process_action(Damage(s, t, amount=1))

285

286 self.winner = s

287

288 return True

289

290 def is_valid(self):

291 return not self.target.dead

292

293

294 class Duel(BaseDuel, InstantSpellCardAction):

295 pass

296

297

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

309

310 def is_valid(self):

311 return not self.target.dead

312

313

314 class MapCannon(ForEach):

315 action_cls = MapCannonEffect

316 group_effect = True

317

318

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

329

330 def is_valid(self):

331 return not self.target.dead

332

333

334 class DemonParade(ForEach):

335 action_cls = DemonParadeEffect

336 group_effect = True

337

338

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

349

350 def is_valid(self):

351 return not self.target.dead

352

353

354 class Feast(ForEach):

355 action_cls = FeastEffect

356 group_effect = True

357

358

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

368

369 card = g.user_input(

370 [tgt],

371 ChooseIndividualCardInputlet(self, cards_avail),

372 trans=pact.trans,

373 ) or random_choose_card(g, [cards_avail])

374

375 migrate_cards([card], tgt.cards)

376

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

378 self.card = card

379 return True

380

381 def is_valid(self):

382 try:

383 cards = ForEach.get_actual_action(self).cards

384 except Exception:

385 return False

386

387 if self.target.dead:

388 return False

389

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

391

392

393 class Harvest(ForEach):

394 action_cls = HarvestEffect

395 group_effect = True

396

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

407

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)

413

414

415 class DollControl(InstantSpellCardAction):

416 card_usage = 'launch'

417

418 def apply_action(self):

419 tl = self.target_list

420 assert len(tl) == 2

421 src = self.source

422

423 attacker, victim = tl

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

425 g = self.game

426

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

434

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)

440

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

444

445 def is_valid(self):

446 if self.target.dead:

447 return False

448

449 assert len(self.target_list) == 2

450

451 attacker, victim = self.target_list

452

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

454 return False

455

456 from .definition import AttackCard

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

458 return False

459

460 return True

461

462

463 class DonationBoxEffect(InstantSpellCardAction):

464 card_usage = 'handover'

465

466 def apply_action(self):

467 t = self.target

468 src = self.source

469 g = self.game

470

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

477

478 if cards:

479 g.players.exclude(t).reveal(cards)

480 migrate_cards(cards, src.showncards)

481

482 return True

483

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)

489

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

495

496

497 class DonationBox(ForEach):

498 action_cls = DonationBoxEffect

499

500 def is_valid(self):

501 tl = self.target_list

502 if not 0 < len(tl) <= 2: return False

503

504 if not all(

505 t.cards or t.showncards or t.equips

506 for t in tl

507 ): return False

508

509 return True

510

511

512 class FrozenFrog(DelayedSpellCardAction, FatetellAction):

513 # 冻青蛙

514 def __init__(self, source, target):

515 self.source = source

516 self.target = target

517 self.fatetell_target = target

518

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

528

529 return True

530

531 return False

532

533 def fatetell_cond(self, c: Card):

534 return c.suit != Card.SPADE

535

536 def fatetell_postprocess(self):

537 g = self.game

538 tgt = self.target

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