# Coverage for thb/thbrole.py : 94%

## 343 statements 325 run 18 missing 0 excluded 9 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

3# -- stdlib --

4 from collections import defaultdict

5 from copy import copy

6 from enum import Enum

7 from itertools import cycle

8 from typing import Any, Dict, List

9 import logging

10 import random

11

12# -- third party --

13# -- own --

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

15 from game.base import Player, get_seed_for, sync_primitive

16 from thb.actions import ActionStageLaunchCard, AskForCard, DistributeCards, DrawCards, DropCardStage

17 from thb.actions import DropCards, GenericAction, LifeLost, PlayerDeath, PlayerTurn, RevealRole

18 from thb.actions import TryRevive, UserAction, ask_for_action, ttags

19 from thb.cards.base import Card, Deck, Skill, VirtualCard

20 from thb.cards.classes import AttackCard, AttackCardRangeHandler, GrazeCard, Heal, TreatAs, t_None

21 from thb.cards.classes import t_One

22 from thb.common import CharChoice, PlayerRole, build_choices

23 from thb.inputlets import ChooseGirlInputlet, ChooseOptionInputlet

24 from thb.item import ImperialRole

25 from thb.mode import THBEventHandler, THBattle

26 from utils.misc import BatchList, classmix

27

28

29# -- code --

30 log = logging.getLogger('THBattleIdentity')

31

32

33 class RoleRevealHandler(THBEventHandler):

34 interested = ['action_apply']

35 execute_before = ['DeathHandler']

36

37 def handle(self, evt_type, act):

38 if evt_type == 'action_apply' and isinstance(act, PlayerDeath):

39 g = self.game

40 tgt = act.target

41

42 g.process_action(RevealRole(g.roles[tgt.player], g.players.player))

43

44 return act

45

46

47 class DeathHandler(THBEventHandler):

48 interested = ['action_apply', 'action_after']

49 game: 'THBattleRole'

50

51 def handle(self, evt_type: str, act) -> Any:

52 if evt_type == 'action_apply' and isinstance(act, PlayerDeath):

53 g = self.game

54 T = THBRoleRole

55 pl = g.players.player

56

57 tgt = act.target

58 dead = lambda p: p.dead or p is tgt

59

60 # curtain's win

61 survivors = [p for p in g.players if not dead(p)]

62 if len(survivors) == 1:

63 pl.reveal([g.roles[p] for p in pl])

64

65 if g.roles[survivors[0].player] == T.CURTAIN:

66 raise GameEnded([survivors[0].player])

67

68 deads: Dict[THBRoleRole, int] = defaultdict(int)

69 for p in g.players:

70 if dead(p):

71 deads[g.roles[p.player].get()] += 1

72

73 def winner(*roles: THBRoleRole):

74 pl.reveal([g.roles[p] for p in pl])

75

76 raise GameEnded([

77 p for p in pl

78 if g.roles[p].get() in roles

79 ])

80

81 def has_no(i: THBRoleRole):

82 return deads[i] == g.roles_config.count(i)

83

84 # attackers' & curtain's win

85 if deads[T.BOSS]:

86 if g.double_curtain: 86 ↛ 87 line 86 didn't jump to line 87, because the condition on line 86 was never true

87 winner(T.ATTACKER)

88 else:

89 if has_no(T.ATTACKER):

90 winner(T.CURTAIN)

91 else:

92 winner(T.ATTACKER)

93

94 # boss & accomplices' win

95 if has_no(T.ATTACKER) and has_no(T.CURTAIN):

96 winner(T.BOSS, T.ACCOMPLICE)

97

98 # all survivors dropped

99 if all([g.is_dropped(ch.player) for ch in survivors]): 99 ↛ 100 line 99 didn't jump to line 100, because the condition on line 99 was never true

100 pl.reveal([g.roles[p] for p in pl])

101 raise GameEnded([])

102

103 elif evt_type == 'action_after' and isinstance(act, PlayerDeath):

104 T = THBRoleRole

105 g = self.game

106 tgt = act.target

107 src = act.source

108

109 if not src:

110 return act

111

112 if g.roles[tgt.player] == T.ATTACKER:

113 g.process_action(DrawCards(src, 3))

114 elif g.roles[tgt.player] == T.ACCOMPLICE:

115 if g.roles[src.player] == T.BOSS:

116 pl = g.players.player

117 pl.exclude(src.player).reveal(list(src.cards))

118

119 cards: List[Card] = []

120 cards.extend(src.cards)

121 cards.extend(src.showncards)

122 cards.extend(src.equips)

123 cards and g.process_action(DropCards(src, src, cards))

124

125 return act

126

127

128 class AssistedAttackCard(TreatAs, VirtualCard):

129 treat_as = AttackCard

130

131

132 class AssistedAttackAction(UserAction):

133 card_usage = 'launch'

134

135 def apply_action(self):

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

137 g = self.game

138 pl = [p for p in g.players if not p.dead and p is not src]

139 p, rst = ask_for_action(self, pl, ('cards', 'showncards'), [], timeout=6)

140 if not p:

141 ttags(src)['assisted_attack_disable'] = True

142 return False

143

144 assert rst

145

146 (c,), _ = rst

147 g.process_action(ActionStageLaunchCard(src, [tgt], AssistedAttackCard.wrap([c], src)))

148

149 return True

150

151 def cond(self, cl):

152 return len(cl) == 1 and cl[0].is_card(AttackCard)

153

154 def is_valid(self):

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

156 act = ActionStageLaunchCard(src, [tgt], AttackCard())

157 disabled = ttags(src)['assisted_attack_disable']

158 return not disabled and act.can_fire()

159

160

161 class AssistedAttack(Skill):

162 associated_action = AssistedAttackAction

163 target = t_One()

164 skill_category = ['character', 'active', 'boss']

165 distance = 1

166

167 def check(self):

168 return not self.associated_cards

169

170

171 class AssistedGraze(Skill):

172 associated_action = None

173 target = t_None()

174 skill_category = ['character', 'passive', 'boss']

175

176

177 class DoNotProcessCard(object):

178

179 def process_card(self, c):

180 return True

181

182

183 class AssistedUseAction(UserAction):

184 def __init__(self, target, afc):

185 self.source = self.target = target

186 self.their_afc_action = afc

187

188 def apply_action(self):

189 tgt = self.target

190 g = self.game

191

192 pl = BatchList([p for p in g.players if not p.dead])

193 pl = pl.rotate_to(tgt)[1:]

194 rst = g.user_input(pl, ChooseOptionInputlet(self, (False, True)), timeout=6, type='all')

195

196 afc = self.their_afc_action

197 for p in pl:

198 if p in rst and rst[p]:

199 act = copy(afc)

200 act.__class__ = classmix(DoNotProcessCard, afc.__class__)

201 act.target = p

202 if g.process_action(act):

203 self.their_afc_action.card = act.card

204 return True

205 else:

206 return False

207

208 return True

209

210

211 class AssistedUseHandler(THBEventHandler):

212 interested = ['action_apply']

213

214 def handle(self, evt_type, act):

215 if evt_type == 'action_apply' and isinstance(act, AskForCard):

216 tgt = act.target

217 if not (tgt.has_skill(self.skill) and issubclass(act.card_cls, self.card_cls)):

218 return act

219

220 if isinstance(act, DoNotProcessCard): 220 ↛ 221 line 220 didn't jump to line 221, because the condition on line 220 was never true

221 return act

222

223 g = self.game

224

225 self.assist_target = tgt

226 if not g.user_input([tgt], ChooseOptionInputlet(self, (False, True))):

227 return act

228

229 g.process_action(AssistedUseAction(tgt, act))

230

231 return act

232

233

234 class AssistedAttackHandler(AssistedUseHandler):

235 skill = AssistedAttack

236 card_cls = AttackCard

237

238

239 class AssistedAttackRangeHandler(AssistedUseHandler):

240 interested = ['calcdistance']

241

242 def handle(self, evt_type, arg):

243 src, card, dist = arg

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

245 if card.is_card(AssistedAttack):

246 AttackCardRangeHandler.fix_attack_range(src, dist)

247

248 return arg

249

250

251 class AssistedGrazeHandler(AssistedUseHandler):

252 skill = AssistedGraze

253 card_cls = GrazeCard

254

255

256 class AssistedHealAction(UserAction):

257 def apply_action(self):

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

259 g = self.game

260 g.process_action(Heal(src, tgt))

261 g.process_action(LifeLost(src, src))

262 return True

263

264

265 class AssistedHealHandler(THBEventHandler):

266 interested = ['action_after']

267

268 def handle(self, evt_type, act):

269 if evt_type == 'action_after' and isinstance(act, TryRevive):

270 if not act.succeeded:

271 return act

272

273 assert act.revived_by

274

275 tgt = act.target

276 if not tgt.has_skill(AssistedHeal):

277 return act

278

279 g = self.game

280

281 self.good_person = p = act.revived_by # for ui

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

283 return act

284

285 g.process_action(AssistedHealAction(p, tgt))

286

287 return act

288

289

290 class AssistedHeal(Skill):

291 associated_action = None

292 target = t_None()

293 skill_category = ['character', 'passive', 'boss']

294

295

296 class ExtraCardSlotHandler(THBEventHandler):

297 interested = ['action_before']

298

299 def handle(self, evt_type, act):

300 if evt_type == 'action_before' and isinstance(act, DropCardStage):

301 tgt = act.target

302 if not tgt.has_skill(ExtraCardSlot):

303 return act

304

305 g = self.game

306 n = sum(i == THBRoleRole.ACCOMPLICE for i in g.roles.values())

307 n -= sum(ch.dead and g.roles[ch.player] == THBRoleRole.ACCOMPLICE for ch in g.players)

308 n = sync_primitive(n, g.players)

309 act.dropn = max(act.dropn - n, 0)

310

311 return act

312

313

314 class ExtraCardSlot(Skill):

315 associated_action = None

316 target = t_None()

317 skill_category = ['character', 'passive', 'boss']

318

319

320 class THBRoleRole(Enum):

321 HIDDEN = 0

322 ATTACKER = 1

323 BOSS = 4

324 ACCOMPLICE = 2

325 CURTAIN = 3

326

327

328 class ChooseBossSkillAction(GenericAction):

329 def apply_action(self) -> bool:

330 g = self.game

331 tgt = self.target

332

333 if tgt.boss_skills: 333 ↛ 334 line 333 didn't jump to line 334, because the condition on line 333 was never true

334 bs = tgt.boss_skills

335 assert len(bs) == 1

336 tgt.skills.extend(bs)

337 self.skill_chosen = bs[0]

338 return True

339

340 self.boss_skills = lst = [ # for ui

341 AssistedAttack,

342 AssistedGraze,

343 AssistedHeal,

344 ExtraCardSlot,

345 ]

346 rst = g.user_input([tgt], ChooseOptionInputlet(self, [i.__name__ for i in lst]))

347 rst = next((i for i in lst if i.__name__ == rst), None) or next(iter(lst))

348 tgt.skills.append(rst)

349 self.skill_chosen = rst # for ui

350 return True

351

352

353 class THBattleRoleBootstrap(BootstrapAction):

354 game: 'THBattleRole'

355

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

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

358 players: BatchList[Player]):

359 self.source = self.target = None

360 self.params = params

361 self.items = items

362 self.players = players

363

364 def apply_action(self) -> bool:

365 g = self.game

366 params = self.params

367

368 g.deck = Deck(g)

369

370 # arrange roles -->

371 g.double_curtain = params['double_curtain']

372

373 B = THBRoleRole.BOSS

374 T = THBRoleRole.ATTACKER

375 A = THBRoleRole.ACCOMPLICE

376 C = THBRoleRole.CURTAIN

377

378 if g.double_curtain: 378 ↛ 379 line 378 didn't jump to line 379, because the condition on line 378 was never true

379 roles = [B, T, T, T, A, A, C, C]

380 else:

381 roles = [B, T, T, T, T, A, A, C]

382

383 orig_pl = self.players

384 pl = BatchList[Player](orig_pl)

385

386 g.roles_config = roles[:]

387

388 imperial_roles = ImperialRole.get_chosen(self.items, pl)

389 for p, i in imperial_roles: 389 ↛ 390 line 389 didn't jump to line 390, because the loop on line 389 never started

390 pl.remove(p)

391 roles.remove(i)

392

393 g.random.shuffle(roles)

394

395 if g.is_client_side():

396 roles = [THBRoleRole.HIDDEN for _ in roles]

397

398 g.roles = {}

399

400 for p, i in imperial_roles + list(zip(pl, roles)):

401 g.roles[p] = PlayerRole(THBRoleRole)

402 g.roles[p].set(i)

403 g.process_action(RevealRole(g.roles[p], p))

404

405 del roles

406

407 is_boss = sync_primitive([g.roles[p] == THBRoleRole.BOSS for p in pl], pl)

408 boss_idx = is_boss.index(True)

409 boss = g.boss = pl[boss_idx]

410

411 g.process_action(RevealRole(g.roles[boss], pl))

412

413 # choose girls init -->

414 from .characters import get_characters

415 pl = pl.rotate_to(boss)

416

417 choices, _ = build_choices(

418 g, pl, self.items,

419 candidates=get_characters('common', 'id', 'id8', '-boss'),

420 spec={boss: {'num': 5, 'akaris': 1}},

421 )

422

423 choices[boss][:0] = [CharChoice(cls) for cls in get_characters('boss')]

424

425 with InputTransaction('ChooseGirl', [boss], mapping=choices) as trans:

426 c: CharChoice = g.user_input([boss], ChooseGirlInputlet(g, choices), 30, 'single', trans)

427

428 c = c or choices[boss][-1]

429 c.chosen = boss

430 c.akari = False

431 pl.reveal(c)

432 trans.notify('girl_chosen', (boss, c))

433 assert c.char_cls

434

435 chars = get_characters('common', 'id', 'id8')

436

437 try:

438 chars.remove(c.char_cls)

439 except Exception:

440 pass

441

442 g.players = BatchList()

443

444 # mix it in advance

445 # so the others could see it

446

447 boss_ch = c.char_cls(boss)

448 g.players.append(boss_ch)

449 g.emit_event('switch_character', (None, boss_ch))

450

451 # boss's hp bonus

452 boss_ch.maxlife += 1

453 boss_ch.life = boss_ch.maxlife

454

455 # choose boss dedicated skill

456 g.process_action(ChooseBossSkillAction(boss_ch, boss_ch))

457

458 # reseat

459 seed = get_seed_for(g, pl)

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

461 g.emit_event('reseat', (orig_pl, pl))

462

463 # others choose girls

464 pl_wo_boss = pl.exclude(boss)

465

466 choices, _ = build_choices(

467 g, pl, self.items,

468 candidates=chars,

469 spec={p: {'num': 4, 'akaris': 1} for p in pl_wo_boss},

470 )

471

472 with InputTransaction('ChooseGirl', pl_wo_boss, mapping=choices) as trans:

473 ilet = ChooseGirlInputlet(g, choices)

474 ilet.with_post_process(lambda p, rst: trans.notify('girl_chosen', (p, rst)) or rst)

475 result = g.user_input(pl_wo_boss, ilet, type='all', trans=trans)

476

477 # mix char class with player -->

478 for p in pl_wo_boss:

479 c = result[p] or choices[p][-1]

480 c.akari = False

481 pl.reveal(c)

482 assert c.char_cls

483 ch = c.char_cls(p)

484 g.players.append(ch)

485 g.emit_event('switch_character', (None, ch))

486

487 assert set(g.players.player) == set(pl)

488 assert len(pl) == g.n_persons

489

490 # -------

491 for ch in g.players:

492 log.info(

493 '>> Player: %s:%s %s',

494 ch.__class__.__name__,

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

496 ch.player.uid,

497 )

498 # -------

499

500 g.refresh_dispatcher()

501 g.emit_event('game_begin', g)

502

503 for p in g.players:

504 g.process_action(DistributeCards(p, amount=4))

505

506 for i, p in enumerate(cycle(g.players.rotate_to(boss_ch))): 506 ↛ 514 line 506 didn't jump to line 514, because the loop on line 506 didn't complete

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

508 if not p.dead:

509 try:

510 g.process_action(PlayerTurn(p))

511 except InterruptActionFlow:

512 pass

513

514 return True

515

516

517 class THBattleRole(THBattle):

518 n_persons = 8

519 game_ehs = [

520 RoleRevealHandler,

521 DeathHandler,

522 AssistedAttackHandler,

523 AssistedAttackRangeHandler,

524 AssistedGrazeHandler,

525 AssistedHealHandler,

526 ExtraCardSlotHandler,

527 ]

528 bootstrap = THBattleRoleBootstrap

529 params_def = {

530 'double_curtain': (False, True),

531 }

532

533 # ----- instance vars -----

534 boss: Player

535 roles_config: List[THBRoleRole]

536 double_curtain: bool

537

538 def can_leave(self, p):

539 return p.dead