Coverage for thb/thbrole.py : 22%
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
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
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
29# -- code --
30log = logging.getLogger('THBattleIdentity')
33class RoleRevealHandler(THBEventHandler):
34 interested = ['action_apply']
35 execute_before = ['DeathHandler']
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
43 g.process_action(RevealRole(p, g.roles[p], g.players.player))
45 return act
48class DeathHandler(THBEventHandler):
49 interested = ['action_apply', 'action_after']
50 game: 'THBattleRole'
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
58 tgt = act.target
59 dead = lambda p: p.dead or p is tgt
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])
66 if g.roles[survivors[0].player] == T.CURTAIN:
67 raise GameEnded([survivors[0].player])
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
74 def winner(*roles: THBRoleRole):
75 pl.reveal([g.roles[p] for p in pl])
77 raise GameEnded([
78 p for p in pl
79 if g.roles[p].get() in roles
80 ])
82 def has_no(i: THBRoleRole):
83 return deads[i] == g.roles_config.count(i)
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)
95 # boss & accomplices' win
96 if has_no(T.ATTACKER) and has_no(T.CURTAIN):
97 winner(T.BOSS, T.ACCOMPLICE)
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([])
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
110 if not src:
111 return act
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))
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))
126 return act
129class AssistedAttackAction(UserAction):
130 card_usage = 'launch'
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
141 assert rst
143 (c,), _ = rst
144 g.process_action(ActionStageLaunchCard(src, [tgt], c, bypass_check=True))
146 return True
148 def cond(self, cl):
149 return len(cl) == 1 and cl[0].is_card(AttackCard)
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()
158class AssistedAttack(Skill):
159 associated_action = AssistedAttackAction
160 target = t_One()
161 skill_category = ['character', 'active', 'boss']
162 distance = 1
164 def check(self):
165 return not self.associated_cards
168class AssistedGraze(Skill):
169 associated_action = None
170 target = t_None()
171 skill_category = ['character', 'passive', 'boss']
174class DoNotProcessCard(object):
176 def process_card(self, c):
177 return True
180class AssistedUseAction(UserAction):
181 def __init__(self, target, afc):
182 self.source = self.target = target
183 self.their_afc_action = afc
185 def apply_action(self):
186 tgt = self.target
187 g = self.game
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')
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
205 return True
208class AssistedUseHandler(THBEventHandler):
209 interested = ['action_apply']
210 skill: ClassVar[Type[Skill]]
211 card_cls: ClassVar[Type[Card]]
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
219 if isinstance(act, DoNotProcessCard):
220 return act
222 g = self.game
224 self.assist_target = tgt
225 if not g.user_input([tgt], ChooseOptionInputlet(self, (False, True))):
226 return act
228 g.process_action(AssistedUseAction(tgt, act))
230 return act
233class AssistedAttackHandler(AssistedUseHandler):
234 skill = AssistedAttack
235 card_cls = AttackCard
238class AssistedAttackRangeHandler(THBEventHandler):
239 interested = ['calcdistance']
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)
247 return arg
250class AssistedGrazeHandler(AssistedUseHandler):
251 skill = AssistedGraze
252 card_cls = GrazeCard
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
264class AssistedHealHandler(THBEventHandler):
265 interested = ['action_after']
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
272 assert act.revived_by
274 tgt = act.target
275 if not tgt.has_skill(AssistedHeal):
276 return act
278 g = self.game
280 self.good_person = p = act.revived_by # for ui
281 if not g.user_input([p], ChooseOptionInputlet(self, (False, True))):
282 return act
284 g.process_action(AssistedHealAction(p, tgt))
286 return act
289class AssistedHeal(Skill):
290 associated_action = None
291 target = t_None()
292 skill_category = ['character', 'passive', 'boss']
295class ExtraCardSlotHandler(THBEventHandler):
296 interested = ['action_before']
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
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)
310 return act
313class ExtraCardSlot(Skill):
314 associated_action = None
315 target = t_None()
316 skill_category = ['character', 'passive', 'boss']
319class THBRoleRole(Enum):
320 HIDDEN = 0
321 ATTACKER = 1
322 BOSS = 4
323 ACCOMPLICE = 2
324 CURTAIN = 3
327class ChooseBossSkillAction(GenericAction):
328 def apply_action(self) -> bool:
329 g = self.game
330 tgt = self.target
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
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
352class THBattleRoleBootstrap(BootstrapAction):
353 game: 'THBattleRole'
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
363 def apply_action(self) -> bool:
364 g = self.game
365 params = self.params
367 g.deck = Deck(g)
369 # arrange roles -->
370 g.double_curtain = params['double_curtain']
372 B = THBRoleRole.BOSS
373 T = THBRoleRole.ATTACKER
374 A = THBRoleRole.ACCOMPLICE
375 C = THBRoleRole.CURTAIN
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]
382 orig_pl = self.players
383 pl = BatchList[Player](orig_pl)
385 g.roles_config = roles[:]
387 imperial_roles = ImperialRole.get_chosen(self.items, pl)
388 for p, i in imperial_roles:
389 pl.remove(p)
390 roles.remove(i)
392 g.random.shuffle(roles)
394 if g.is_client_side():
395 roles = [THBRoleRole.HIDDEN for _ in roles]
397 g.roles = {}
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]))
404 del roles
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]
410 g.process_action(RevealRole(boss, g.roles[boss], pl))
412 # choose girls init -->
413 from .characters import get_characters
414 pl = pl.rotate_to(boss)
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 )
422 choices[boss][:0] = [CharChoice(cls) for cls in get_characters('boss')]
424 with InputTransaction('ChooseGirl', [boss], mapping=choices) as trans:
425 c: CharChoice = g.user_input([boss], ChooseGirlInputlet(g, choices), 30, 'single', trans)
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
434 chars = get_characters('common', 'id', 'id8')
436 try:
437 chars.remove(c.char_cls)
438 except Exception:
439 pass
441 g.players = BatchList()
443 # mix it in advance
444 # so the others could see it
446 boss_ch = c.char_cls(boss)
447 g.players.append(boss_ch)
448 g.emit_event('switch_character', (None, boss_ch))
450 # boss's hp bonus
451 boss_ch.maxlife += 1
452 boss_ch.life = boss_ch.maxlife
454 # choose boss dedicated skill
455 g.process_action(ChooseBossSkillAction(boss_ch, boss_ch))
457 # reseat
458 seed = get_seed_for(g, pl)
459 random.Random(seed).shuffle(pl)
460 g.emit_event('reseat', (orig_pl, pl))
462 # others choose girls
463 pl_wo_boss = pl.exclude(boss)
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 )
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)
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))
486 assert set(g.players.player) == set(pl)
487 assert len(pl) == g.n_persons
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 # -------
499 g.refresh_dispatcher()
500 g.emit_event('game_begin', g)
502 for p in g.players:
503 g.process_action(DistributeCards(p, amount=4))
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
513 return True
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 }
532 # ----- instance vars -----
533 boss: Player
534 roles_config: List[THBRoleRole]
535 double_curtain: bool
537 def can_leave(self, p):
538 return p.dead