Coverage for thb/thb2v2.py : 91%
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 enum import Enum
6from itertools import cycle
7from typing import Any, Dict, List, Set, Type
8import logging
9import random
11# -- third party --
12# -- own --
13from game.base import BootstrapAction, GameEnded, GameItem, InputTransaction, InterruptActionFlow
14from game.base import Player, get_seed_for
15from thb.actions import DeadDropCards, DistributeCards, DrawCardStage, DrawCards
16from thb.actions import MigrateCardsTransaction, PlayerDeath, PlayerTurn, RevealRole, UserAction
17from thb.actions import migrate_cards
18from thb.cards.base import Deck
19from thb.characters.base import Character
20from thb.common import CharChoice, PlayerRole, roll
21from thb.inputlets import ChooseGirlInputlet, ChooseOptionInputlet
22from thb.mode import THBEventHandler, THBattle
23from utils.misc import BatchList, partition
24import settings
27# -- code --
28log = logging.getLogger('THBattle2v2')
31class DeathHandler(THBEventHandler):
32 interested = ['action_apply']
34 def handle(self, evt_type, act):
35 if evt_type != 'action_apply': return act 35 ↛ exitline 35 didn't return from function 'handle', because the return on line 35 wasn't executed
36 if not isinstance(act, PlayerDeath): return act
38 g = self.game
39 tgt = act.target
41 tgt = act.target
42 dead = lambda ch: ch.dead or g.is_dropped(ch.player) or ch is tgt
44 # see if game ended
45 force1, force2 = list(g.forces.values())
46 if all(dead(ch) for ch in force1):
47 raise GameEnded(force2.player)
49 if all(dead(ch) for ch in force2):
50 raise GameEnded(force1.player)
52 return act
55class HeritageAction(UserAction):
56 def apply_action(self):
57 src, tgt = self.source, self.target
58 lists = [tgt.cards, tgt.showncards, tgt.equips]
59 with MigrateCardsTransaction(self) as trans:
60 for cl in lists:
61 if not cl: continue
62 cl = list(cl)
63 src.reveal(cl)
64 migrate_cards(cl, src.cards, unwrap=True, trans=trans)
66 return True
69class HeritageHandler(THBEventHandler):
70 interested = ['action_before']
71 execute_after = ['DeathHandler', 'SadistHandler']
73 def handle(self, evt_type, act):
74 if evt_type != 'action_before': return act 74 ↛ exitline 74 didn't return from function 'handle', because the return on line 74 wasn't executed
75 if not isinstance(act, DeadDropCards): return act
77 g = self.game
78 tgt = act.target
79 for f in g.forces.values(): 79 ↛ 83line 79 didn't jump to line 83, because the loop on line 79 didn't complete
80 if tgt in f:
81 break
82 else:
83 assert False, 'WTF?!'
85 other = BatchList(f).exclude(tgt)[0]
86 if other.dead: return act 86 ↛ exitline 86 didn't return from function 'handle', because the return on line 86 wasn't executed
88 if g.user_input([other], ChooseOptionInputlet(self, ('inherit', 'draw'))) == 'inherit':
89 g.process_action(HeritageAction(other, tgt))
91 else:
92 g.process_action(DrawCards(other, 2))
94 return act
97class ExtraCardHandler(THBEventHandler):
98 interested = ['action_before']
100 def handle(self, evt_type, act):
101 if evt_type != 'action_before': 101 ↛ 102line 101 didn't jump to line 102, because the condition on line 101 was never true
102 return act
104 if not isinstance(act, DrawCardStage):
105 return act
107 g = self.game
108 if g.draw_extra: 108 ↛ 109line 108 didn't jump to line 109, because the condition on line 108 was never true
109 act.amount += 1
111 return act
114class THB2v2Role(Enum):
115 HIDDEN = 0
116 HAKUREI = 1
117 MORIYA = 2
120class THBattle2v2Bootstrap(BootstrapAction):
121 game: 'THBattle2v2'
123 def __init__(self, params: Dict[str, Any],
124 items: Dict[Player, List[GameItem]],
125 players: BatchList[Player]):
126 self.source = self.target = None
127 self.params = params
128 self.items = items
129 self.players = players
131 def apply_action(self) -> bool:
132 g = self.game
133 params = self.params
134 items = self.items
136 pl = self.players
138 g.deck = Deck(g)
139 g.roles = {}
141 if params['random_force']: 141 ↛ 145line 141 didn't jump to line 145, because the condition on line 141 was never false
142 seed = get_seed_for(g, pl)
143 random.Random(seed).shuffle(pl)
145 g.draw_extra = params['draw_extra_card']
147 H, M = THB2v2Role.HAKUREI, THB2v2Role.MORIYA
148 g.forces = {H: BatchList(), M: BatchList()}
150 for p, id in zip(pl, [H, H, M, M]):
151 g.roles[p] = r = PlayerRole(THB2v2Role)
152 g.roles[p].set(id)
153 g.process_action(RevealRole(r, pl))
155 roll_rst = roll(g, pl, items)
156 '''
157 winner = g.forces[roll_rst[0].identity.value]
158 f1, f2 = partition(lambda ch: g.forces[ch.identity.value] is winner, roll_rst)
159 final_order = [f1[0], f2[0], f2[1], f1[1]]
160 '''
161 g.emit_event('reseat', (pl, roll_rst))
162 pl = roll_rst
164 # ban / choose girls -->
165 from . import characters
166 chars = characters.get_characters('common', '2v2')
168 seed = get_seed_for(g, pl)
169 random.Random(seed).shuffle(chars)
171 testing: List[str] = list(settings.TESTING_CHARACTERS)
172 testing, chars = partition(lambda c: c.__name__ in testing, chars)
173 chars.extend(testing)
175 chars = chars[-20:]
176 choices = [CharChoice(cls) for cls in chars]
178 banned: Set[Type[Character]] = set()
179 mapping = {p: choices for p in pl}
180 with InputTransaction('BanGirl', pl, mapping=mapping) as trans:
181 for p in pl:
182 c: CharChoice
183 c = g.user_input([p], ChooseGirlInputlet(self, mapping), timeout=30, trans=trans)
184 c = c or next(_c for _c in choices if not _c.chosen) 184 ↛ exitline 184 didn't finish the generator expression on line 184
185 c.chosen = p
186 cls = c.char_cls
187 assert cls
188 banned.add(cls)
189 trans.notify('girl_chosen', (p, c))
191 assert len(banned) == 4
193 chars = [_c for _c in chars if _c not in banned]
195 g.random.shuffle(chars)
197 if g.is_client_side():
198 chars = [None] * len(chars)
200 mapping: Dict[Player, List[CharChoice]] = {}
202 for p in pl:
203 mapping[p] = [CharChoice(cls) for cls in chars[-4:]]
204 mapping[p][-1].akari = True
206 del chars[-4:]
208 p.reveal(mapping[p])
210 g.pause(1)
212 with InputTransaction('ChooseGirl', pl, mapping=mapping) as trans:
213 ilet = ChooseGirlInputlet(g, mapping)
215 @ilet.with_post_process
216 def process(p, c):
217 c = c or mapping[p][0]
218 trans.notify('girl_chosen', (p, c))
219 return c
221 rst = g.user_input(pl, ilet, timeout=30, type='all', trans=trans)
223 # reveal
224 g.players = BatchList()
226 for p in pl:
227 c = rst[p]
228 c.akari = False
229 pl.reveal(c)
230 assert c.char_cls
231 ch = c.char_cls(p)
232 g.players.append(ch)
234 g.refresh_dispatcher()
236 for p, ch in zip(pl, g.players):
237 assert p is ch.player
238 g.emit_event('switch_character', (p, ch))
240 # -------
241 for ch in g.players:
242 log.info(
243 '>> Player: %s:%s',
244 ch.__class__.__name__,
245 g.roles[ch.player].get().name,
246 )
247 # -------
249 g.forces = {}
250 for ch in g.players:
251 g.forces.setdefault(g.roles[ch.player].get(), BatchList()).append(ch)
253 g.emit_event('game_begin', g)
255 for ch in g.players:
256 g.process_action(DistributeCards(ch, amount=4))
258 for i, ch in enumerate(cycle(g.players)): 258 ↛ 266line 258 didn't jump to line 266, because the loop on line 258 didn't complete
259 if i >= 6000: break 259 ↛ 266line 259 didn't jump to line 266, because the break on line 259 wasn't executed
260 if not ch.dead:
261 try:
262 g.process_action(PlayerTurn(ch))
263 except InterruptActionFlow:
264 pass
266 return True
269class THBattle2v2(THBattle):
270 n_persons = 4
271 game_ehs = [
272 DeathHandler,
273 HeritageHandler,
274 ExtraCardHandler,
275 ]
276 bootstrap = THBattle2v2Bootstrap
277 params_def = {
278 'random_force': (True, False),
279 'draw_extra_card': (False, True),
280 }
282 forces: Dict[THB2v2Role, BatchList[Character]]
284 draw_extra: bool
286 def can_leave(g, p: Player):
287 for ch in g.players:
288 if ch.player is p:
289 return ch.dead
290 else:
291 return False