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 -*-
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
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
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
42 g.process_action(RevealRole(g.roles[tgt.player], g.players.player))
44 return act
47class DeathHandler(THBEventHandler):
48 interested = ['action_apply', 'action_after']
49 game: 'THBattleRole'
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
57 tgt = act.target
58 dead = lambda p: p.dead or p is tgt
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])
65 if g.roles[survivors[0].player] == T.CURTAIN:
66 raise GameEnded([survivors[0].player])
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
73 def winner(*roles: THBRoleRole):
74 pl.reveal([g.roles[p] for p in pl])
76 raise GameEnded([
77 p for p in pl
78 if g.roles[p].get() in roles
79 ])
81 def has_no(i: THBRoleRole):
82 return deads[i] == g.roles_config.count(i)
84 # attackers' & curtain's win
85 if deads[T.BOSS]:
86 if g.double_curtain:
87 winner(T.ATTACKER)
88 else:
89 if has_no(T.ATTACKER):
90 winner(T.CURTAIN)
91 else:
92 winner(T.ATTACKER)
94 # boss & accomplices' win
95 if has_no(T.ATTACKER) and has_no(T.CURTAIN):
96 winner(T.BOSS, T.ACCOMPLICE)
98 # all survivors dropped
99 if all([g.is_dropped(ch.player) for ch in survivors]):
100 pl.reveal([g.roles[p] for p in pl])
101 raise GameEnded([])
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
109 if not src:
110 return act
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))
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))
125 return act
128class AssistedAttackCard(TreatAs, VirtualCard):
129 treat_as = AttackCard
132class AssistedAttackAction(UserAction):
133 card_usage = 'launch'
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
144 assert rst
146 (c,), _ = rst
147 g.process_action(ActionStageLaunchCard(src, [tgt], AssistedAttackCard.wrap([c], src)))
149 return True
151 def cond(self, cl):
152 return len(cl) == 1 and cl[0].is_card(AttackCard)
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()
161class AssistedAttack(Skill):
162 associated_action = AssistedAttackAction
163 target = t_One()
164 skill_category = ['character', 'active', 'boss']
165 distance = 1
167 def check(self):
168 return not self.associated_cards
171class AssistedGraze(Skill):
172 associated_action = None
173 target = t_None()
174 skill_category = ['character', 'passive', 'boss']
177class DoNotProcessCard(object):
179 def process_card(self, c):
180 return True
183class AssistedUseAction(UserAction):
184 def __init__(self, target, afc):
185 self.source = self.target = target
186 self.their_afc_action = afc
188 def apply_action(self):
189 tgt = self.target
190 g = self.game
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')
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
208 return True
211class AssistedUseHandler(THBEventHandler):
212 interested = ['action_apply']
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
220 if isinstance(act, DoNotProcessCard):
221 return act
223 g = self.game
225 self.assist_target = tgt
226 if not g.user_input([tgt], ChooseOptionInputlet(self, (False, True))):
227 return act
229 g.process_action(AssistedUseAction(tgt, act))
231 return act
234class AssistedAttackHandler(AssistedUseHandler):
235 skill = AssistedAttack
236 card_cls = AttackCard
239class AssistedAttackRangeHandler(AssistedUseHandler):
240 interested = ['calcdistance']
242 def handle(self, evt_type, arg):
243 src, card, dist = arg
244 if evt_type == 'calcdistance':
245 if card.is_card(AssistedAttack):
246 AttackCardRangeHandler.fix_attack_range(src, dist)
248 return arg
251class AssistedGrazeHandler(AssistedUseHandler):
252 skill = AssistedGraze
253 card_cls = GrazeCard
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
265class AssistedHealHandler(THBEventHandler):
266 interested = ['action_after']
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
273 assert act.revived_by
275 tgt = act.target
276 if not tgt.has_skill(AssistedHeal):
277 return act
279 g = self.game
281 self.good_person = p = act.revived_by # for ui
282 if not g.user_input([p], ChooseOptionInputlet(self, (False, True))):
283 return act
285 g.process_action(AssistedHealAction(p, tgt))
287 return act
290class AssistedHeal(Skill):
291 associated_action = None
292 target = t_None()
293 skill_category = ['character', 'passive', 'boss']
296class ExtraCardSlotHandler(THBEventHandler):
297 interested = ['action_before']
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
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)
311 return act
314class ExtraCardSlot(Skill):
315 associated_action = None
316 target = t_None()
317 skill_category = ['character', 'passive', 'boss']
320class THBRoleRole(Enum):
321 HIDDEN = 0
322 ATTACKER = 1
323 BOSS = 4
324 ACCOMPLICE = 2
325 CURTAIN = 3
328class ChooseBossSkillAction(GenericAction):
329 def apply_action(self) -> bool:
330 g = self.game
331 tgt = self.target
333 if tgt.boss_skills:
334 bs = tgt.boss_skills
335 assert len(bs) == 1
336 tgt.skills.extend(bs)
337 self.skill_chosen = bs[0]
338 return True
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
353class THBattleRoleBootstrap(BootstrapAction):
354 game: 'THBattleRole'
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
364 def apply_action(self) -> bool:
365 g = self.game
366 params = self.params
368 g.deck = Deck(g)
370 # arrange roles -->
371 g.double_curtain = params['double_curtain']
373 B = THBRoleRole.BOSS
374 T = THBRoleRole.ATTACKER
375 A = THBRoleRole.ACCOMPLICE
376 C = THBRoleRole.CURTAIN
378 if g.double_curtain:
379 roles = [B, T, T, T, A, A, C, C]
380 else:
381 roles = [B, T, T, T, T, A, A, C]
383 orig_pl = self.players
384 pl = BatchList[Player](orig_pl)
386 g.roles_config = roles[:]
388 imperial_roles = ImperialRole.get_chosen(self.items, pl)
389 for p, i in imperial_roles:
390 pl.remove(p)
391 roles.remove(i)
393 g.random.shuffle(roles)
395 if g.is_client_side():
396 roles = [THBRoleRole.HIDDEN for _ in roles]
398 g.roles = {}
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))
405 del roles
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]
411 g.process_action(RevealRole(g.roles[boss], pl))
413 # choose girls init -->
414 from .characters import get_characters
415 pl = pl.rotate_to(boss)
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 )
423 choices[boss][:0] = [CharChoice(cls) for cls in get_characters('boss')]
425 with InputTransaction('ChooseGirl', [boss], mapping=choices) as trans:
426 c: CharChoice = g.user_input([boss], ChooseGirlInputlet(g, choices), 30, 'single', trans)
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
435 chars = get_characters('common', 'id', 'id8')
437 try:
438 chars.remove(c.char_cls)
439 except Exception:
440 pass
442 g.players = BatchList()
444 # mix it in advance
445 # so the others could see it
447 boss_ch = c.char_cls(boss)
448 g.players.append(boss_ch)
449 g.emit_event('switch_character', (None, boss_ch))
451 # boss's hp bonus
452 boss_ch.maxlife += 1
453 boss_ch.life = boss_ch.maxlife
455 # choose boss dedicated skill
456 g.process_action(ChooseBossSkillAction(boss_ch, boss_ch))
458 # reseat
459 seed = get_seed_for(g, pl)
460 random.Random(seed).shuffle(pl)
461 g.emit_event('reseat', (orig_pl, pl))
463 # others choose girls
464 pl_wo_boss = pl.exclude(boss)
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 )
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)
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))
487 assert set(g.players.player) == set(pl)
488 assert len(pl) == g.n_persons
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 # -------
500 g.refresh_dispatcher()
501 g.emit_event('game_begin', g)
503 for p in g.players:
504 g.process_action(DistributeCards(p, amount=4))
506 for i, p in enumerate(cycle(g.players.rotate_to(boss_ch))):
507 if i >= 6000: break
508 if not p.dead:
509 try:
510 g.process_action(PlayerTurn(p))
511 except InterruptActionFlow:
512 pass
514 return True
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 }
533 # ----- instance vars -----
534 boss: Player
535 roles_config: List[THBRoleRole]
536 double_curtain: bool
538 def can_leave(self, p):
539 return p.dead