# Coverage for thb/thb2v2.py : 91%

## 184 statements 175 run 9 missing 0 excluded 10 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 enum import Enum

6 from itertools import cycle

7 from typing import Any, Dict, List, Set, Type

8 import logging

9 import random

10

11# -- third party --

12# -- own --

13 from game.base import BootstrapAction, GameEnded, GameItem, InputTransaction, InterruptActionFlow

14 from game.base import Player, get_seed_for

15 from thb.actions import DeadDropCards, DistributeCards, DrawCardStage, DrawCards

16 from thb.actions import MigrateCardsTransaction, PlayerDeath, PlayerTurn, RevealRole, UserAction

17 from thb.actions import migrate_cards

18 from thb.cards.base import Deck

19 from thb.characters.base import Character

20 from thb.common import CharChoice, PlayerRole, roll

21 from thb.inputlets import ChooseGirlInputlet, ChooseOptionInputlet

22 from thb.mode import THBEventHandler, THBattle

23 from utils.misc import BatchList, partition

24 import settings

25

26

27# -- code --

28 log = logging.getLogger('THBattle2v2')

29

30

31 class DeathHandler(THBEventHandler):

32 interested = ['action_apply']

33

34 def handle(self, evt_type, act):

35 if evt_type != 'action_apply': return act 35 ↛ exit line 35 didn't return from function 'handle', because the return on line 35 wasn't executed

36 if not isinstance(act, PlayerDeath): return act

37

38 g = self.game

39 tgt = act.target

40

41 tgt = act.target

42 dead = lambda ch: ch.dead or g.is_dropped(ch.player) or ch is tgt

43

44 # see if game ended

45 force1, force2 = list(g.forces.values())

46 if all(dead(ch) for ch in force1):

47 raise GameEnded(force2.player)

48

49 if all(dead(ch) for ch in force2):

50 raise GameEnded(force1.player)

51

52 return act

53

54

55 class HeritageAction(UserAction):

56 def apply_action(self):

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

58 lists = [tgt.cards, tgt.showncards, tgt.equips]

59 with MigrateCardsTransaction(self) as trans:

60 for cl in lists:

61 if not cl: continue

62 cl = list(cl)

63 src.reveal(cl)

64 migrate_cards(cl, src.cards, unwrap=True, trans=trans)

65

66 return True

67

68

69 class HeritageHandler(THBEventHandler):

70 interested = ['action_before']

71 execute_after = ['DeathHandler', 'SadistHandler']

72

73 def handle(self, evt_type, act):

74 if evt_type != 'action_before': return act 74 ↛ exit line 74 didn't return from function 'handle', because the return on line 74 wasn't executed

75 if not isinstance(act, DeadDropCards): return act

76

77 g = self.game

78 tgt = act.target

79 for f in g.forces.values(): 79 ↛ 83 line 79 didn't jump to line 83, because the loop on line 79 didn't complete

80 if tgt in f:

81 break

82 else:

83 assert False, 'WTF?!'

84

85 other = BatchList(f).exclude(tgt)[0]

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

87

88 if g.user_input([other], ChooseOptionInputlet(self, ('inherit', 'draw'))) == 'inherit':

89 g.process_action(HeritageAction(other, tgt))

90

91 else:

92 g.process_action(DrawCards(other, 2))

93

94 return act

95

96

97 class ExtraCardHandler(THBEventHandler):

98 interested = ['action_before']

99

100 def handle(self, evt_type, act):

101 if evt_type != 'action_before': 101 ↛ 102 line 101 didn't jump to line 102, because the condition on line 101 was never true

102 return act

103

104 if not isinstance(act, DrawCardStage):

105 return act

106

107 g = self.game

108 if g.draw_extra: 108 ↛ 109 line 108 didn't jump to line 109, because the condition on line 108 was never true

109 act.amount += 1

110

111 return act

112

113

114 class THB2v2Role(Enum):

115 HIDDEN = 0

116 HAKUREI = 1

117 MORIYA = 2

118

119

120 class THBattle2v2Bootstrap(BootstrapAction):

121 game: 'THBattle2v2'

122

123 def __init__(self, params: Dict[str, Any],

124 items: Dict[Player, List[GameItem]],

125 players: BatchList[Player]):

126 self.source = self.target = None

127 self.params = params

128 self.items = items

129 self.players = players

130

131 def apply_action(self) -> bool:

132 g = self.game

133 params = self.params

134 items = self.items

135

136 pl = self.players

137

138 g.deck = Deck(g)

139 g.roles = {}

140

141 if params['random_force']: 141 ↛ 145 line 141 didn't jump to line 145, because the condition on line 141 was never false

142 seed = get_seed_for(g, pl)

143 random.Random(seed).shuffle(pl)

144

145 g.draw_extra = params['draw_extra_card']

146

147 H, M = THB2v2Role.HAKUREI, THB2v2Role.MORIYA

148 g.forces = {H: BatchList(), M: BatchList()}

149

150 for p, id in zip(pl, [H, H, M, M]):

151 g.roles[p] = r = PlayerRole(THB2v2Role)

152 g.roles[p].set(id)

153 g.process_action(RevealRole(r, pl))

154

155 roll_rst = roll(g, pl, items)

156 '''

157 winner = g.forces[roll_rst[0].identity.value]

158 f1, f2 = partition(lambda ch: g.forces[ch.identity.value] is winner, roll_rst)

159 final_order = [f1[0], f2[0], f2[1], f1[1]]

160 '''

161 g.emit_event('reseat', (pl, roll_rst))

162 pl = roll_rst

163

164 # ban / choose girls -->

165 from . import characters

166 chars = characters.get_characters('common', '2v2')

167

168 seed = get_seed_for(g, pl)

169 random.Random(seed).shuffle(chars)

170

171 testing: List[str] = list(settings.TESTING_CHARACTERS)

172 testing, chars = partition(lambda c: c.__name__ in testing, chars)

173 chars.extend(testing)

174

175 chars = chars[-20:]

176 choices = [CharChoice(cls) for cls in chars]

177

178 banned: Set[Type[Character]] = set()

179 mapping = {p: choices for p in pl}

180 with InputTransaction('BanGirl', pl, mapping=mapping) as trans:

181 for p in pl:

182 c: CharChoice

183 c = g.user_input([p], ChooseGirlInputlet(self, mapping), timeout=30, trans=trans)

184 c = c or next(_c for _c in choices if not _c.chosen) 184 ↛ exit line 184 didn't finish the generator expression on line 184

185 c.chosen = p

186 cls = c.char_cls

187 assert cls

188 banned.add(cls)

189 trans.notify('girl_chosen', (p, c))

190

191 assert len(banned) == 4

192

193 chars = [_c for _c in chars if _c not in banned]

194

195 g.random.shuffle(chars)

196

197 if g.is_client_side():

198 chars = [None] * len(chars)

199

200 mapping: Dict[Player, List[CharChoice]] = {}

201

202 for p in pl:

203 mapping[p] = [CharChoice(cls) for cls in chars[-4:]]

204 mapping[p][-1].akari = True

205

206 del chars[-4:]

207

208 p.reveal(mapping[p])

209

210 g.pause(1)

211

212 with InputTransaction('ChooseGirl', pl, mapping=mapping) as trans:

213 ilet = ChooseGirlInputlet(g, mapping)

214

215 @ilet.with_post_process

216 def process(p, c):

217 c = c or mapping[p][0]

218 trans.notify('girl_chosen', (p, c))

219 return c

220

221 rst = g.user_input(pl, ilet, timeout=30, type='all', trans=trans)

222

223 # reveal

224 g.players = BatchList()

225

226 for p in pl:

227 c = rst[p]

228 c.akari = False

229 pl.reveal(c)

230 assert c.char_cls

231 ch = c.char_cls(p)

232 g.players.append(ch)

233

234 g.refresh_dispatcher()

235

236 for p, ch in zip(pl, g.players):

237 assert p is ch.player

238 g.emit_event('switch_character', (p, ch))

239

240 # -------

241 for ch in g.players:

242 log.info(

243 '>> Player: %s:%s',

244 ch.__class__.__name__,

245 g.roles[ch.player].get().name,

246 )

247 # -------

248

249 g.forces = {}

250 for ch in g.players:

251 g.forces.setdefault(g.roles[ch.player].get(), BatchList()).append(ch)

252

253 g.emit_event('game_begin', g)

254

255 for ch in g.players:

256 g.process_action(DistributeCards(ch, amount=4))

257

258 for i, ch in enumerate(cycle(g.players)): 258 ↛ 266 line 258 didn't jump to line 266, because the loop on line 258 didn't complete

259 if i >= 6000: break 259 ↛ 266 line 259 didn't jump to line 266, because the break on line 259 wasn't executed

260 if not ch.dead:

261 try:

262 g.process_action(PlayerTurn(ch))

263 except InterruptActionFlow:

264 pass

265

266 return True

267

268

269 class THBattle2v2(THBattle):

270 n_persons = 4

271 game_ehs = [

272 DeathHandler,

273 HeritageHandler,

274 ExtraCardHandler,

275 ]

276 bootstrap = THBattle2v2Bootstrap

277 params_def = {

278 'random_force': (True, False),

279 'draw_extra_card': (False, True),

280 }

281

282 forces: Dict[THB2v2Role, BatchList[Character]]

283

284 draw_extra: bool

285

286 def can_leave(g, p: Player):

287 for ch in g.players:

288 if ch.player is p:

289 return ch.dead

290 else:

291 return False