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

2 

3# -- stdlib -- 

4from typing import Sequence 

5 

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 

18 

19 

20# -- code -- 

21class SpellCardAction(UserAction): 

22 non_responsive = False 

23 

24 

25class InstantSpellCardAction(SpellCardAction): 

26 pass 

27 

28 

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

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 

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 

64 

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 

70 

71 

72@register_eh 

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

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 

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

137 

138 

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

154 

155 

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

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

195class SinsackDamage(Damage): 

196 pass 

197 

198 

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

234 

235 

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

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

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

283 

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

286 

287 self.winner = s 

288 

289 return True 

290 

291 def is_valid(self): 

292 return not self.target.dead 

293 

294 

295class Duel(BaseDuel, InstantSpellCardAction): 

296 pass 

297 

298 

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 

310 

311 def is_valid(self): 

312 return not self.target.dead 

313 

314 

315class MapCannon(ForEach): 

316 action_cls = MapCannonEffect 

317 group_effect = True 

318 

319 

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 

330 

331 def is_valid(self): 

332 return not self.target.dead 

333 

334 

335class DemonParade(ForEach): 

336 action_cls = DemonParadeEffect 

337 group_effect = True 

338 

339 

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 

350 

351 def is_valid(self): 

352 return not self.target.dead 

353 

354 

355class Feast(ForEach): 

356 action_cls = FeastEffect 

357 group_effect = True 

358 

359 

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 

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

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

416class 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 ↛ 448line 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 

464class 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 ↛ 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: 

477 cards = [random_choose_card(g, cats)] 

478 

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) 

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

495 return False 

496 

497 

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

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