# Coverage for thb/cards/basic.py : 98%

## 239 statements 237 run 2 missing 0 excluded 6 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 cast

6

7# -- third party --

8# -- own --

9 from game.base import EventHandler

10 from thb.actions import ActionStage, ActionStageLaunchCard, AskForCard, Damage, DropCards, ForEach

11 from thb.actions import GenericAction, LaunchCard, MigrateCardsTransaction, PrepareStage, UseCard

12 from thb.actions import UserAction, VitalityLimitExceeded, register_eh, user_choose_cards

13

14

15# -- code --

16 class BasicAction(UserAction):

17 pass

18

19

20 class BaseAttack(UserAction):

21

22 def __init__(self, source, target, damage=1):

23 self.source = source

24 self.target = target

25 self.damage = damage

26

27 def apply_action(self):

28 g = self.game

29 source, target = self.source, self.target

30 rst = g.process_action(LaunchGraze(target))

31 self1, rst = g.emit_event('attack_aftergraze', (self, not rst))

32 assert self1 is self

33 assert rst in (False, True)

34 if rst:

35 g.process_action(Damage(source, target, amount=self.damage))

36 return True

37 else:

38 return False

39

40 def is_valid(self):

41 return not self.target.dead

42

43

44 class Attack(BaseAttack, BasicAction):

45 pass

46

47

48 class InevitableAttack(Attack):

49 def apply_action(self):

50 g = self.game

51 dmg = Damage(self.source, self.target, amount=self.damage)

52 g.process_action(dmg)

53 return True

54

55

56@register_eh

57 class AttackCardRangeHandler(EventHandler):

58 interested = ['calcdistance']

59

60 def handle(self, evt_type, act):

61 if evt_type == 'calcdistance': 61 ↛ 67 line 61 didn't jump to line 67, because the condition on line 61 was never false

62 src, card, dist = act

63 from .definition import AttackCard

64 if card.is_card(AttackCard):

65 self.fix_attack_range(src, dist)

66

67 return act

68

69 @staticmethod

70 def attack_range_bonus(p):

71 from .equipment import WeaponSkill

72 l = [

73 s.range - 1 for s in p.skills

74 if issubclass(s, WeaponSkill) and p.has_skill(s)

75 ]

76 return max(0, 0, *l)

77

78 @classmethod

79 def fix_attack_range(cls, src, dist):

80 l = cls.attack_range_bonus(src)

81 for p in dist:

82 dist[p] -= l

83

84

85@register_eh

86 class AttackCardVitalityHandler(EventHandler):

87 interested = ['action_before', 'action_shootdown']

88

89 @classmethod

90 def handle(cls, evt_type, act):

91 if evt_type == 'action_before' and isinstance(act, ActionStageLaunchCard):

92 from .definition import AttackCard

93 src = act.source

94 if act.card.is_card(AttackCard) and not cls.is_disabled(src):

95 act._[cls] = 'already-handled'

96 src.tags['vitality'] -= 1

97

98 elif evt_type == 'action_shootdown' and isinstance(act, ActionStageLaunchCard):

99 from .definition import AttackCard

100 if act.card.is_card(AttackCard):

101 src = act.source

102 if cls.is_disabled(src):

103 return act

104

105 if act._[cls]:

106 return act

107

108 if src.tags['vitality'] > 0:

109 return act

110

111 raise VitalityLimitExceeded

112

113 return act

114

115 @staticmethod

116 def disable(p):

117 p.tags['attack_card_vitality'] = p.tags['turn_count']

118

119 @staticmethod

120 def enable(p):

121 p.tags['attack_card_vitality'] = 0

122

123 @staticmethod

124 def is_disabled(p):

125 return p.tags['attack_card_vitality'] >= p.tags['turn_count']

126

127

128@register_eh

129 class VitalityHandler(EventHandler):

130 interested = ['action_before']

131

132 def handle(self, evt_type, act):

133 if evt_type == 'action_before' and isinstance(act, ActionStage):

134 act.source.tags['vitality'] = 1

135

136 return act

137

138

139 class Heal(BasicAction):

140

141 def __init__(self, source, target, amount=1):

142 self.source = source

143 self.target = target

144 self.amount = amount

145

146 def apply_action(self):

147 target = self.target

148 if target.life < target.maxlife: 148 ↛ 152 line 148 didn't jump to line 152, because the condition on line 148 was never false

149 target.life = min(target.life + self.amount, target.maxlife)

150 return True

151 else:

152 return False

153

154 def is_valid(self):

155 tgt = self.target

156 return not tgt.dead and tgt.life < tgt.maxlife

157

158

159 class GrazeAction(BasicAction):

160

161 def apply_action(self):

162 return True

163

164

165 class UseAttack(AskForCard):

166 card_usage = 'use'

167

168 def __init__(self, target):

169 from thb.cards.definition import AttackCard

170 AskForCard.__init__(self, target, target, AttackCard)

171

172 def process_card(self, card):

173 g = self.game

174 return g.process_action(UseCard(self.target, card))

175

176 def ask_for_action_verify(self, p, cl, tl):

177 return UseCard(p, cl[0]).can_fire()

178

179

180 class BaseUseGraze(AskForCard):

181 def __init__(self, target):

182 from thb.cards.definition import GrazeCard

183 AskForCard.__init__(self, target, target, GrazeCard)

184

185

186 class UseGraze(BaseUseGraze):

187 card_usage = 'use'

188

189 def process_card(self, card):

190 g = self.game

191 return g.process_action(UseCard(self.target, card))

192

193 def ask_for_action_verify(self, p, cl, tl):

194 return UseCard(p, cl[0]).can_fire()

195

196

197 class LaunchGraze(BaseUseGraze):

198 card_usage = 'launch'

199

200 def process_card(self, card):

201 g = self.game

202 tgt = self.target

203 return g.process_action(LaunchCard(tgt, [tgt], card, GrazeAction(tgt, tgt)))

204

205 def ask_for_action_verify(self, p, cl, tl):

206 tgt = self.target

207 return LaunchCard(tgt, [tgt], cl[0], GrazeAction(tgt, tgt)).can_fire()

208

209

210 class AskForHeal(AskForCard):

211 card_usage = 'launch'

212

213 def __init__(self, source, target):

214 from thb import cards

215 AskForCard.__init__(self, source, target, cards.definition.HealCard)

216

217 def process_card(self, card):

218 g = self.game

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

220 heal_cls = card.associated_action or Heal

221 return g.process_action(LaunchCard(tgt, [src], card, heal_cls(tgt, src)))

222

223 def ask_for_action_verify(self, p, cl, tl):

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

225 card = cl[0]

226 heal_cls = card.associated_action or Heal

227 return LaunchCard(tgt, [src], card, heal_cls(tgt, src)).can_fire()

228

229

230 class Wine(BasicAction):

231 def apply_action(self):

232 self.target.tags['wine'] = True

233 return True

234

235 def is_valid(self):

236 return not self.target.dead

237

238

239 class SoberUp(GenericAction):

240 def apply_action(self):

241 self.target.tags['wine'] = False

242 return True

243

244

245 class WineRevive(GenericAction):

246 def __init__(self, act):

247 self.act = act

248 self.source = act.target

249 self.target = act.target

250

251 def apply_action(self):

252 self.act.amount -= 1

253 tgt = self.target

254 self.game.process_action(SoberUp(tgt, tgt))

255 return True

256

257

258@register_eh

259 class WineHandler(EventHandler):

260 interested = ['action_apply', 'action_before', 'post_choose_target']

261

262 def handle(self, evt_type, act):

263 if evt_type == 'action_before' and isinstance(act, BaseAttack):

264 pact = ForEach.get_actual_action(act) or act

265 if getattr(pact, 'in_wine', False):

266 act.damage += 1

267

268 elif evt_type == 'post_choose_target':

269 act, tl = arg = act

270

271 from thb.cards.definition import AttackCard

272 if act.card.is_card(AttackCard):

273 src = act.source

274 if src.tags['wine']:

275 self.game.process_action(SoberUp(src, src))

276 act.card_action.in_wine = True

277

278 return arg

279

280 elif evt_type == 'action_apply' and isinstance(act, PrepareStage):

281 src = act.target

282 if src.tags['wine']:

283 self.game.process_action(SoberUp(src, src))

284

285 elif evt_type == 'action_before' and isinstance(act, Damage):

286 if act.cancelled: return act 286 ↛ exit line 286 didn't return from function 'handle', because the return on line 286 wasn't executed

287 if act.amount < 1: return act

288 tgt = act.target

289 if act.amount >= tgt.life and tgt.tags['wine']:

290 g = self.game

291 g.process_action(WineRevive(act))

292

293 return act

294

295

296 class Exinwan(BasicAction):

297 # 恶心丸

298 def apply_action(self):

299 return True

300

301

302 class ExinwanEffect(GenericAction):

303 # 恶心丸

304 card_usage = 'drop'

305

306 def apply_action(self):

307 g = self.game

308 tgt = self.target

309 if tgt.dead: 309 ↛ 310 line 309 didn't jump to line 310, because the condition on line 309 was never true

310 return False

311

312 cards = user_choose_cards(self, tgt, ('cards', 'showncards', 'equips'))

313

314 if cards:

315 g.process_action(DropCards(tgt, tgt, cards))

316 else:

317 g.process_action(Damage(None, tgt))

318

319 return True

320

321 def cond(self, cards):

322 if len(cards) != 2: return False

323 from thb.cards.base import Skill

324 if any(isinstance(c, Skill) for c in cards): return False 324 ↛ exit line 324 didn't return from function 'cond', because the return on line 324 wasn't executed

325 return True

326

327 def is_valid(self):

328 return not self.target.dead

329

330

331@register_eh

332 class ExinwanHandler(EventHandler):

333 # 恶心丸

334

335 interested = ['post_card_migration']

336

337 def handle(self, evt_type, arg) -> None:

338 from thb.cards.base import VirtualCard, HiddenCard

339 from thb.cards.definition import ExinwanCard

340

341 if evt_type == 'post_card_migration': 341 ↛ 363 line 341 didn't jump to line 363, because the condition on line 341 was never false

342 moves = cast(MigrateCardsTransaction, arg).movements

343 moves = [m for m in moves if m.to.type == 'droppedcard']

344

345 # cards to dropped area should all unwrapped

346 assert not any(

347 m.card.is_card(VirtualCard) or m.card.is_card(HiddenCard)

348 for m in moves

349 )

350

351 moves = [m for m in moves if m.card.is_card(ExinwanCard)]

352

353 # no same card dropped twice in the same transaction

354 assert len(moves) == len(set(m.card for m in moves))

355

356 for m in moves:

357 tgt = m.trans.action.source

358 if tgt and not tgt.dead:

359 act = ExinwanEffect(tgt, tgt)

360 act.associated_card = m.card

361 self.game.process_action(act)

362

363 return arg