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

2 

3# -- stdlib -- 

4from collections import defaultdict 

5from copy import copy 

6from enum import Enum 

7from itertools import cycle 

8from typing import Any, Dict, List 

9import logging 

10import random 

11 

12# -- third party -- 

13# -- own -- 

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

15from game.base import Player, get_seed_for, sync_primitive 

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

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

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

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

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

21from thb.cards.classes import 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 

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

43 

44 return act 

45 

46 

47class 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 ↛ 87line 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 ↛ 100line 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 

128class AssistedAttackCard(TreatAs, VirtualCard): 

129 treat_as = AttackCard 

130 

131 

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

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

171class AssistedGraze(Skill): 

172 associated_action = None 

173 target = t_None() 

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

175 

176 

177class DoNotProcessCard(object): 

178 

179 def process_card(self, c): 

180 return True 

181 

182 

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

211class 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 ↛ 221line 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 

234class AssistedAttackHandler(AssistedUseHandler): 

235 skill = AssistedAttack 

236 card_cls = AttackCard 

237 

238 

239class AssistedAttackRangeHandler(AssistedUseHandler): 

240 interested = ['calcdistance'] 

241 

242 def handle(self, evt_type, arg): 

243 src, card, dist = arg 

244 if evt_type == 'calcdistance': 244 ↛ 248line 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 

251class AssistedGrazeHandler(AssistedUseHandler): 

252 skill = AssistedGraze 

253 card_cls = GrazeCard 

254 

255 

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

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

290class AssistedHeal(Skill): 

291 associated_action = None 

292 target = t_None() 

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

294 

295 

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

314class ExtraCardSlot(Skill): 

315 associated_action = None 

316 target = t_None() 

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

318 

319 

320class THBRoleRole(Enum): 

321 HIDDEN = 0 

322 ATTACKER = 1 

323 BOSS = 4 

324 ACCOMPLICE = 2 

325 CURTAIN = 3 

326 

327 

328class ChooseBossSkillAction(GenericAction): 

329 def apply_action(self) -> bool: 

330 g = self.game 

331 tgt = self.target 

332 

333 if tgt.boss_skills: 333 ↛ 334line 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 

353class 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 ↛ 379line 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 ↛ 390line 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 ↛ 514line 506 didn't jump to line 514, because the loop on line 506 didn't complete

507 if i >= 6000: break 507 ↛ 514line 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 

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