# Coverage for thb/thbrole.py : 22%

## 344 statements 100 run 244 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 from collections import defaultdict

6 from copy import copy

7 from enum import Enum

8 from itertools import cycle

9 from typing import Any, ClassVar, Dict, List, Type

10 import logging

11 import random

12

13# -- third party --

14# -- own --

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

16 from game.base import Player, get_seed_for, sync_primitive

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

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

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

20 from thb.cards.base import Card, Deck, Skill

21 from thb.cards.classes import AttackCard, AttackCardRangeHandler, GrazeCard, Heal, t_None, 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 p = tgt.player

42

43 g.process_action(RevealRole(p, g.roles[p], g.players.player))

44

45 return act

46

47

48 class DeathHandler(THBEventHandler):

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

50 game: 'THBattleRole'

51

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

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

54 g = self.game

55 T = THBRoleRole

56 pl = g.players.player

57

58 tgt = act.target

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

60

61 # curtain's win

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

63 if len(survivors) == 1:

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

65

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

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

68

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

70 for p in g.players:

71 if dead(p):

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

73

74 def winner(*roles: THBRoleRole):

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

76

77 raise GameEnded([

78 p for p in pl

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

80 ])

81

82 def has_no(i: THBRoleRole):

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

84

85 # attackers' & curtain's win

86 if deads[T.BOSS]:

87 if g.double_curtain:

88 winner(T.ATTACKER)

89 else:

90 if has_no(T.ATTACKER):

91 winner(T.CURTAIN)

92 else:

93 winner(T.ATTACKER)

94

95 # boss & accomplices' win

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

97 winner(T.BOSS, T.ACCOMPLICE)

98

99 # all survivors dropped

100 if all([g.is_dropped(ch.player) for ch in survivors]):

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

102 raise GameEnded([])

103

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

105 T = THBRoleRole

106 g = self.game

107 tgt = act.target

108 src = act.source

109

110 if not src:

111 return act

112

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

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

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

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

117 pl = g.players.player

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

119

120 cards: List[Card] = []

121 cards.extend(src.cards)

122 cards.extend(src.showncards)

123 cards.extend(src.equips)

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

125

126 return act

127

128

129 class AssistedAttackAction(UserAction):

130 card_usage = 'launch'

131

132 def apply_action(self):

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

134 g = self.game

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

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

137 if not p:

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

139 return False

140

141 assert rst

142

143 (c,), _ = rst

144 g.process_action(ActionStageLaunchCard(src, [tgt], c, bypass_check=True))

145

146 return True

147

148 def cond(self, cl):

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

150

151 def is_valid(self):

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

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

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

155 return not disabled and act.can_fire()

156

157

158 class AssistedAttack(Skill):

159 associated_action = AssistedAttackAction

160 target = t_One()

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

162 distance = 1

163

164 def check(self):

165 return not self.associated_cards

166

167

168 class AssistedGraze(Skill):

169 associated_action = None

170 target = t_None()

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

172

173

174 class DoNotProcessCard(object):

175

176 def process_card(self, c):

177 return True

178

179

180 class AssistedUseAction(UserAction):

181 def __init__(self, target, afc):

182 self.source = self.target = target

183 self.their_afc_action = afc

184

185 def apply_action(self):

186 tgt = self.target

187 g = self.game

188

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

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

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

192

193 afc = self.their_afc_action

194 for p in pl:

195 if p in rst and rst[p]:

196 act = copy(afc)

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

198 act.target = p

199 if g.process_action(act):

200 self.their_afc_action.card = act.card

201 return True

202 else:

203 return False

204

205 return True

206

207

208 class AssistedUseHandler(THBEventHandler):

209 interested = ['action_apply']

210 skill: ClassVar[Type[Skill]]

211 card_cls: ClassVar[Type[Card]]

212

213 def handle(self, evt_type, act):

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

215 tgt = act.target

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

217 return act

218

219 if isinstance(act, DoNotProcessCard):

220 return act

221

222 g = self.game

223

224 self.assist_target = tgt

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

226 return act

227

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

229

230 return act

231

232

233 class AssistedAttackHandler(AssistedUseHandler):

234 skill = AssistedAttack

235 card_cls = AttackCard

236

237

238 class AssistedAttackRangeHandler(THBEventHandler):

239 interested = ['calcdistance']

240

241 def handle(self, evt_type, arg):

242 src, card, dist = arg

243 if evt_type == 'calcdistance':

244 if card.is_card(AssistedAttack):

245 AttackCardRangeHandler.fix_attack_range(src, dist)

246

247 return arg

248

249

250 class AssistedGrazeHandler(AssistedUseHandler):

251 skill = AssistedGraze

252 card_cls = GrazeCard

253

254

255 class AssistedHealAction(UserAction):

256 def apply_action(self):

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

258 g = self.game

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

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

261 return True

262

263

264 class AssistedHealHandler(THBEventHandler):

265 interested = ['action_after']

266

267 def handle(self, evt_type, act):

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

269 if not act.succeeded:

270 return act

271

272 assert act.revived_by

273

274 tgt = act.target

275 if not tgt.has_skill(AssistedHeal):

276 return act

277

278 g = self.game

279

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

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

282 return act

283

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

285

286 return act

287

288

289 class AssistedHeal(Skill):

290 associated_action = None

291 target = t_None()

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

293

294

295 class ExtraCardSlotHandler(THBEventHandler):

296 interested = ['action_before']

297

298 def handle(self, evt_type, act):

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

300 tgt = act.target

301 if not tgt.has_skill(ExtraCardSlot):

302 return act

303

304 g = self.game

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

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

307 n = sync_primitive(n, g.players.player)

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

309

310 return act

311

312

313 class ExtraCardSlot(Skill):

314 associated_action = None

315 target = t_None()

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

317

318

319 class THBRoleRole(Enum):

320 HIDDEN = 0

321 ATTACKER = 1

322 BOSS = 4

323 ACCOMPLICE = 2

324 CURTAIN = 3

325

326

327 class ChooseBossSkillAction(GenericAction):

328 def apply_action(self) -> bool:

329 g = self.game

330 tgt = self.target

331

332 if tgt.boss_skills:

333 bs = tgt.boss_skills

334 assert len(bs) == 1

335 tgt.skills.extend(bs)

336 self.skill_chosen = bs[0]

337 return True

338

339 self.boss_skills = lst = [ # for ui

340 AssistedAttack,

341 AssistedGraze,

342 AssistedHeal,

343 ExtraCardSlot,

344 ]

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

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

347 tgt.skills.append(rst)

348 self.skill_chosen = rst # for ui

349 return True

350

351

352 class THBattleRoleBootstrap(BootstrapAction):

353 game: 'THBattleRole'

354

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

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

357 players: BatchList[Player]):

358 self.source = self.target = None

359 self.params = params

360 self.items = items

361 self.players = players

362

363 def apply_action(self) -> bool:

364 g = self.game

365 params = self.params

366

367 g.deck = Deck(g)

368

369 # arrange roles -->

370 g.double_curtain = params['double_curtain']

371

372 B = THBRoleRole.BOSS

373 T = THBRoleRole.ATTACKER

374 A = THBRoleRole.ACCOMPLICE

375 C = THBRoleRole.CURTAIN

376

377 if g.double_curtain:

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

379 else:

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

381

382 orig_pl = self.players

383 pl = BatchList[Player](orig_pl)

384

385 g.roles_config = roles[:]

386

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

388 for p, i in imperial_roles:

389 pl.remove(p)

390 roles.remove(i)

391

392 g.random.shuffle(roles)

393

394 if g.is_client_side():

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

396

397 g.roles = {}

398

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

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

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

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

403

404 del roles

405

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

407 boss_idx = is_boss.index(True)

408 boss = g.boss = pl[boss_idx]

409

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

411

412 # choose girls init -->

413 from .characters import get_characters

414 pl = pl.rotate_to(boss)

415

416 choices, _ = build_choices(

417 g, pl, self.items,

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

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

420 )

421

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

423

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

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

426

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

428 c.chosen = boss

429 c.akari = False

430 pl.reveal(c)

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

432 assert c.char_cls

433

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

435

436 try:

437 chars.remove(c.char_cls)

438 except Exception:

439 pass

440

441 g.players = BatchList()

442

443 # mix it in advance

444 # so the others could see it

445

446 boss_ch = c.char_cls(boss)

447 g.players.append(boss_ch)

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

449

450 # boss's hp bonus

451 boss_ch.maxlife += 1

452 boss_ch.life = boss_ch.maxlife

453

454 # choose boss dedicated skill

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

456

457 # reseat

458 seed = get_seed_for(g, pl)

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

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

461

462 # others choose girls

463 pl_wo_boss = pl.exclude(boss)

464

465 choices, _ = build_choices(

466 g, pl, self.items,

467 candidates=chars,

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

469 )

470

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

472 ilet = ChooseGirlInputlet(g, choices)

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

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

475

476 # mix char class with player -->

477 for p in pl_wo_boss:

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

479 c.akari = False

480 pl.reveal(c)

481 assert c.char_cls

482 ch = c.char_cls(p)

483 g.players.append(ch)

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

485

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

487 assert len(pl) == g.n_persons

488

489 # -------

490 for ch in g.players:

491 log.info(

492 '>> Player: %s:%s %s',

493 ch.__class__.__name__,

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

495 ch.player.uid,

496 )

497 # -------

498

499 g.refresh_dispatcher()

500 g.emit_event('game_begin', g)

501

502 for p in g.players:

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

504

505 for i, p in enumerate(cycle(g.players.rotate_to(boss_ch))):

506 if i >= 6000: break

507 if not p.dead:

508 try:

509 g.process_action(PlayerTurn(p))

510 except InterruptActionFlow:

511 pass

512

513 return True

514

515

516 class THBattleRole(THBattle):

517 n_persons = 8

518 game_ehs = [

519 RoleRevealHandler,

520 DeathHandler,

521 AssistedAttackHandler,

522 AssistedAttackRangeHandler,

523 AssistedGrazeHandler,

524 AssistedHealHandler,

525 ExtraCardSlotHandler,

526 ]

527 bootstrap = THBattleRoleBootstrap

528 params_def = {

529 'double_curtain': (False, True),

530 }

531

532 # ----- instance vars -----

533 boss: Player

534 roles_config: List[THBRoleRole]

535 double_curtain: bool

536

537 def can_leave(self, p):

538 return p.dead