Hide keyboard shortcuts

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 -*- 

2from __future__ import annotations 

3 

4# -- stdlib -- 

5from collections import defaultdict 

6from copy import copy 

7from enum import Enum 

8from itertools import cycle 

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

10import logging 

11import random 

12 

13# -- third party -- 

14# -- own -- 

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

16from game.base import Player, get_seed_for, sync_primitive 

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

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

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

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

21from thb.cards.classes import AttackCard, AttackCardRangeHandler, GrazeCard, Heal, t_None, t_One 

22from thb.common import CharChoice, PlayerRole, build_choices 

23from thb.inputlets import ChooseGirlInputlet, ChooseOptionInputlet 

24from thb.item import ImperialRole 

25from thb.mode import THBEventHandler, THBattle 

26from utils.misc import BatchList, classmix 

27 

28 

29# -- code -- 

30log = logging.getLogger('THBattleIdentity') 

31 

32 

33class 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 

48class 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 

129class 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 

158class 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 

168class AssistedGraze(Skill): 

169 associated_action = None 

170 target = t_None() 

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

172 

173 

174class DoNotProcessCard(object): 

175 

176 def process_card(self, c): 

177 return True 

178 

179 

180class 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 

208class 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 

233class AssistedAttackHandler(AssistedUseHandler): 

234 skill = AssistedAttack 

235 card_cls = AttackCard 

236 

237 

238class 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 

250class AssistedGrazeHandler(AssistedUseHandler): 

251 skill = AssistedGraze 

252 card_cls = GrazeCard 

253 

254 

255class 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 

264class 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 

289class AssistedHeal(Skill): 

290 associated_action = None 

291 target = t_None() 

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

293 

294 

295class 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 

313class ExtraCardSlot(Skill): 

314 associated_action = None 

315 target = t_None() 

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

317 

318 

319class THBRoleRole(Enum): 

320 HIDDEN = 0 

321 ATTACKER = 1 

322 BOSS = 4 

323 ACCOMPLICE = 2 

324 CURTAIN = 3 

325 

326 

327class 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 

352class 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 

516class 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