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

## 242 statements 94 run 148 missing 0 excluded 8 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

11 from thb.actions import FinalizeStage, ForEach, GenericAction, LaunchCard, MigrateCardsTransaction

12 from thb.actions import PrepareStage, UseCard, UserAction, VitalityLimitExceeded, register_eh

13 from thb.actions import user_choose_cards

14

15

16# -- code --

17 class BasicAction(UserAction):

18 pass

19

20

21 class BaseAttack(UserAction):

22

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

24 self.source = source

25 self.target = target

26 self.damage = damage

27

28 def apply_action(self):

29 g = self.game

30 source, target = self.source, self.target

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

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

33 assert self1 is self

34 assert rst in (False, True)

35 if rst:

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

37 return True

38 else:

39 return False

40

41 def is_valid(self):

42 return not self.target.dead

43

44

45 class Attack(BaseAttack, BasicAction):

46 pass

47

48

49 class InevitableAttack(Attack):

50 def apply_action(self):

51 g = self.game

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

53 g.process_action(dmg)

54 return True

55

56

57@register_eh

58 class AttackCardRangeHandler(EventHandler):

59 interested = ['calcdistance']

60

61 def handle(self, evt_type, act):

62 if evt_type == 'calcdistance':

63 src, card, dist = act

64 from .definition import AttackCard

65 if card.is_card(AttackCard):

66 self.fix_attack_range(src, dist)

67

68 return act

69

70 @staticmethod

71 def attack_range_bonus(p):

72 from .equipment import WeaponSkill

73 l = [

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

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

76 ]

77 return max(0, 0, *l)

78

79 @classmethod

80 def fix_attack_range(cls, src, dist):

81 l = cls.attack_range_bonus(src)

82 for p in dist:

83 dist[p] -= l

84

85

86@register_eh

87 class AttackCardVitalityHandler(EventHandler):

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

89

90 def handle(self, evt_type, act):

91 if evt_type == 'action_before' and isinstance(act, ActionStageLaunchCard): 91 ↛ 92 line 91 didn't jump to line 92, because the condition on line 91 was never true

92 from .definition import AttackCard

93 src = act.source

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

95 act._['vitality-handler'] = 'already-handled'

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

97

98 elif evt_type == 'action_shootdown' and isinstance(act, ActionStageLaunchCard): 98 ↛ 99 line 98 didn't jump to line 99, because the condition on line 98 was never true

99 from .definition import AttackCard

100 if act.card.is_card(AttackCard):

101 src = act.source

102 if self.is_disabled(src):

103 return act

104

105 if act._['vitality-handler']:

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): 133 ↛ 134 line 133 didn't jump to line 134, because the condition on line 133 was never true

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

135 elif evt_type == 'action_before' and isinstance(act, FinalizeStage): 135 ↛ 136 line 135 didn't jump to line 136, because the condition on line 135 was never true

136 act.source.tags['vitality'] = 0

137

138 return act

139

140

141 class Heal(BasicAction):

142

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

144 self.source = source

145 self.target = target

146 self.amount = amount

147

148 def apply_action(self):

149 target = self.target

150 if target.life < target.maxlife:

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

152 return True

153 else:

154 return False

155

156 def is_valid(self):

157 tgt = self.target

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

159

160

161 class GrazeAction(BasicAction):

162

163 def apply_action(self):

164 return True

165

166

167 class UseAttack(AskForCard):

168 card_usage = 'use'

169

170 def __init__(self, target):

171 from thb.cards.definition import AttackCard

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

173

174 def process_card(self, card):

175 g = self.game

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

177

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

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

180

181

182 class BaseUseGraze(AskForCard):

183 def __init__(self, target):

184 from thb.cards.definition import GrazeCard

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

186

187

188 class UseGraze(BaseUseGraze):

189 card_usage = 'use'

190

191 def process_card(self, card):

192 g = self.game

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

194

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

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

197

198

199 class LaunchGraze(BaseUseGraze):

200 card_usage = 'launch'

201

202 def process_card(self, card):

203 g = self.game

204 tgt = self.target

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

206

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

208 tgt = self.target

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

210

211

212 class AskForHeal(AskForCard):

213 card_usage = 'launch'

214

215 def __init__(self, source, target):

216 from thb import cards

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

218

219 def process_card(self, card):

220 g = self.game

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

222 heal_cls = card.associated_action or Heal

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

224

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

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

227 card = cl[0]

228 heal_cls = card.associated_action or Heal

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

230

231

232 class Wine(BasicAction):

233 def apply_action(self):

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

235 return True

236

237 def is_valid(self):

238 return not self.target.dead

239

240

241 class SoberUp(GenericAction):

242 def apply_action(self):

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

244 return True

245

246

247 class WineRevive(GenericAction):

248 def __init__(self, act):

249 self.act = act

250 self.source = act.target

251 self.target = act.target

252

253 def apply_action(self):

254 self.act.amount -= 1

255 tgt = self.target

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

257 return True

258

259

260@register_eh

261 class WineHandler(EventHandler):

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

263

264 def handle(self, evt_type, act):

265 if evt_type == 'action_before' and isinstance(act, BaseAttack): 265 ↛ 266 line 265 didn't jump to line 266, because the condition on line 265 was never true

266 pact = ForEach.get_actual_action(act) or act

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

268 act.damage += 1

269

270 elif evt_type == 'post_choose_target': 270 ↛ 271 line 270 didn't jump to line 271, because the condition on line 270 was never true

271 act, tl = arg = act

272

273 from thb.cards.definition import AttackCard

274 if act.card.is_card(AttackCard):

275 src = act.source

276 if src.tags['wine']:

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

278 act.card_action.in_wine = True

279

280 return arg

281

282 elif evt_type == 'action_apply' and isinstance(act, PrepareStage): 282 ↛ 283 line 282 didn't jump to line 283, because the condition on line 282 was never true

283 src = act.target

284 if src.tags['wine']:

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

286

287 elif evt_type == 'action_before' and isinstance(act, Damage): 287 ↛ 288 line 287 didn't jump to line 288, because the condition on line 287 was never true

288 if act.cancelled: return act

289 if act.amount < 1: return act

290 tgt = act.target

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

292 g = self.game

293 g.process_action(WineRevive(act))

294

295 return act

296

297

298 class Exinwan(BasicAction):

299 # 恶心丸

300 def apply_action(self):

301 return True

302

303

304 class ExinwanEffect(GenericAction):

305 # 恶心丸

306 card_usage = 'drop'

307

308 def apply_action(self):

309 g = self.game

310 tgt = self.target

311 if tgt.dead:

312 return False

313

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

315

316 if cards:

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

318 else:

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

320

321 return True

322

323 def cond(self, cards):

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

325 from thb.cards.base import Skill

326 if any(isinstance(c, Skill) for c in cards): return False

327 return True

328

329 def is_valid(self):

330 return not self.target.dead

331

332

333@register_eh

334 class ExinwanHandler(EventHandler):

335 # 恶心丸

336

337 interested = ['post_card_migration']

338

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

340 from thb.cards.base import VirtualCard, HiddenCard

341 from thb.cards.definition import ExinwanCard

342

343 if evt_type == 'post_card_migration':

344 moves = cast(MigrateCardsTransaction, arg).movements

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

346

347 # cards to dropped area should all unwrapped

348 assert not any(

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

350 for m in moves

351 )

352

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

354

355 # no same card dropped twice in the same transaction

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

357

358 for m in moves:

359 tgt = m.trans.action.source

360 if tgt and not tgt.dead:

361 act = ExinwanEffect(tgt, tgt)

362 act.associated_card = m.card

363 self.game.process_action(act)

364

365 return arg