# Coverage for thb/characters/momiji.py : 25%

## 184 statements 63 run 121 missing 0 excluded 0 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 import itertools

6

7# -- third party --

8# -- own --

9 from game.base import sync_primitive

10 from thb.actions import ActionStage, ActionStageLaunchCard, AskForCard, Damage, FinalizeStage

11 from thb.actions import GenericAction, LaunchCard, ShowCards, UserAction, migrate_cards, ttags

12 from thb.cards.base import CardList, Skill, VirtualCard

13 from thb.cards.classes import AttackCard, DollControlCard, DuelCard, TreatAs, t_None

14 from thb.characters.base import Character, register_character_to

15 from thb.inputlets import ChooseOptionInputlet

16 from thb.mode import THBAction, THBEventHandler

17

18

19# -- code --

20 class DisarmHideAction(UserAction):

21 def __init__(self, source, target, cards):

22 self.source = source

23 self.target = target

24 self.cards = cards

25

26 def apply_action(self):

27 tgt = self.target

28 cl = tgt._.get('momoji-sentry-cards')

29 if cl is None:

30 cl = CardList(tgt, 'momiji-sentry-cards')

31 tgt._['momiji-sentry-cards'] = cl

32 tgt.lists.append(cl)

33

34 migrate_cards(self.cards, cl)

35 return True

36

37

38 class DisarmReturningAction(GenericAction):

39 def apply_action(self):

40 tgt = self.target

41 cl = tgt._.get('momiji-sentry-cards')

42 cl and migrate_cards(cl, tgt.cards, unwrap=True)

43 return True

44

45

46 class DisarmHandler(THBEventHandler):

47 interested = ['action_after']

48 execute_after = ['DyingHandler']

49

50 card_usage = 'launch'

51

52 def handle(self, evt_type, act):

53 if evt_type == 'action_after' and isinstance(act, Damage):

54 g = self.game

55 src, tgt = act.source, act.target

56 if not (src and src.has_skill(Disarm)): return act

57 if tgt.dead: return act

58 pact = g.action_stack[-1]

59 pcard = getattr(pact, 'associated_card', None)

60 if not pcard: return act

61

62 assert isinstance(pact, THBAction)

63

64 if not pcard.is_card(AttackCard) and not (pcard.is_card(DuelCard) and pact.source is src):

65 return act

66

67 if not g.user_input([src], ChooseOptionInputlet(self, (False, True))):

68 return act

69

70 cl = list(tgt.cards) + list(tgt.showncards)

71 g.process_action(ShowCards(tgt, cl, [src]))

72

73 if g.is_server_side():

74 l = [bool(c.is_card(AttackCard) or 'spellcard' in c.category) for c in cl]

75 else:

76 l = [False for c in cl]

77

78 l = sync_primitive(l, g.players.player)

79 cl = list(itertools.compress(cl, l))

80 g.process_action(DisarmHideAction(src, tgt, cl))

81

82 elif evt_type == 'action_after' and isinstance(act, FinalizeStage):

83 tgt = act.target

84 g = self.game

85 g.process_action(DisarmReturningAction(tgt, tgt))

86

87 return act

88

89

90 class Disarm(Skill):

91 associated_action = None

92 skill_category = ['character', 'passive', 'compulsory']

93 target = t_None()

94

95

96 class SentryAction(AskForCard):

97 card_usage = 'launch'

98

99 def __init__(self, source, target):

100 AskForCard.__init__(self, source, source, AttackCard)

101 self.victim = target

102

103 def process_card(self, c):

104 g = self.game

105 src, tgt = self.source, self.victim

106 c = SentryAttack.wrap([c], src)

107 return g.process_action(LaunchCard(src, [tgt], c))

108

109

110 class SentryHandler(THBEventHandler):

111 interested = ['action_apply']

112

113 def handle(self, evt_type, act):

114 if evt_type == 'action_apply' and isinstance(act, ActionStage):

115 g = self.game

116 for p in g.players:

117 if p.dead: continue

118 if not p.has_skill(Sentry): continue

119

120 tgt = act.target

121 if p is tgt: continue

122 self.target = tgt # for ui

123

124 dist = LaunchCard.calc_distance(g, p, AttackCard())

125 if dist.get(tgt, 1) > 0: continue

126 if not g.user_input([p], ChooseOptionInputlet(self, (False, True))):

127 continue

128

129 g.process_action(SentryAction(p, tgt))

130

131 return act

132

133

134 class SentryAttack(TreatAs, VirtualCard):

135 treat_as = AttackCard

136

137

138 class Sentry(Skill):

139 associated_action = None

140 skill_category = ['character', 'passive']

141 target = t_None()

142

143

144 class TelegnosisHandler(THBEventHandler):

145 interested = ['calcdistance']

146 execute_after = ['AttackCardHandler', 'UFODistanceHandler']

147

148 processing = False

149

150 def handle(self, evt_type, arg):

151 if self.processing:

152 return arg

153

154 elif evt_type == 'calcdistance':

155 src, c, dist = arg

156 if not src.has_skill(Telegnosis): return arg

157 if not c.is_card(AttackCard): return arg

158

159 g = self.game

160

161 try:

162 self.processing = True

163 for p in dist:

164 if p is src: continue

165 d = LaunchCard.calc_distance(g, p, AttackCard())

166 if d[src] <= 0:

167 dist[p] = 0

168

169 finally:

170 self.processing = False

171

172 return arg

173

174

175 class Telegnosis(Skill):

176 associated_action = None

177 skill_category = ['character', 'passive', 'compulsory']

178 target = t_None()

179

180

181 class SolidShieldHandler(THBEventHandler):

182 interested = ['action_before']

183 execute_after = ['AttackCardHandler']

184

185 def handle(self, evt_type, act):

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

187 src, tgt = act.source, act.target

188 if not src or not tgt: return act

189 ttags(src)['solid_shield_launch_count'] += 1

190 if ttags(src)['solid_shield_launch_count'] != 1:

191 return act

192

193 if src is tgt: return act

194

195 if tgt.dead: return act

196

197 c = act.card

198 if not (c.is_card(AttackCard) or 'instant_spellcard' in c.category):

199 return act

200

201 g = self.game

202 for p in g.players.rotate_to(src):

203 if p is src:

204 continue

205

206 if p is tgt:

207 continue

208

209 if not p.has_skill(SolidShield):

210 continue

211

212 dist = LaunchCard.calc_distance(g, p, SolidShield(p))

213 if dist[tgt] > 0:

214 continue

215

216 cond = c.is_card(AttackCard) or 'instant_spellcard' in c.category

217 cond = cond and (c.is_card(DollControlCard) or len(act.target_list) == 1) # HACK HERE!

218 if not cond:

219 return act

220

221 if g.user_input([p], ChooseOptionInputlet(self, (False, True))):

222 g.process_action(SolidShieldAction(p, src, act))

223 break

224

225 return act

226

227

228 class SolidShieldAction(UserAction):

229 def __init__(self, source, target, action):

230 self.source = source

231 self.target = target

232 self.action = action

233

234 def apply_action(self):

235 tgt = self.target

236 lc = self.action

237 assert isinstance(lc, ActionStageLaunchCard)

238

239 cl = tgt._.get('momoji-sentry-cards')

240 if cl is None:

241 cl = CardList(tgt, 'momiji-sentry-cards')

242 tgt._['momiji-sentry-cards'] = cl

243 tgt.lists.append(cl)

244

245 migrate_cards([lc.card], cl, unwrap=True)

246 lc.cancelled = True

247 return True

248

249

250 class SolidShield(Skill):

251 distance = 1

252 associated_action = None

253 skill_category = ['character', 'passive', 'compulsory']

254 target = t_None()

255

256

257@register_character_to('common')

258 class Momiji(Character):

259 # skills = [Disarm, Sentry, Telegnosis]

260 skills = [Disarm, Sentry, SolidShield]

261 # eventhandlers = [SentryHandler, DisarmHandler, TelegnosisHandler]

262 eventhandlers = [SentryHandler, DisarmHandler, SolidShieldHandler]

263 maxlife = 4