# Coverage for thb/common.py : 39%

## 123 statements 60 run 63 missing 4 excluded 1 partial

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 from __future__ import annotations

3

4# -- stdlib --

5 from enum import Enum

6 from itertools import cycle

7 from typing import Any, Dict, Generic, Iterable, List, Optional, Sequence, TYPE_CHECKING, Tuple

8 from typing import Type, TypeVar

9 import logging

10 import random

11

12# -- third party --

13 from mypy_extensions import TypedDict

14

15# -- own --

16 from game.base import GameViralContext, Player, get_seed_for, sync_primitive

17 from thb.item import GameItem

18 from thb.mode import THBattle

19 from utils.misc import BatchList, partition

20 import settings

21

22# -- typing --

23 if TYPE_CHECKING:

24 from thb.characters.base import Character # noqa: F401

25

26

27# -- code --

28 log = logging.getLogger('thb.common')

29

30

31 class CharChoice(GameViralContext):

32 chosen: Any = None

33 char_cls: Optional[Type[Character]]

34 akari: bool = False

35

36 def __init__(self, char_cls=None, akari=False) -> None:

37 self.set(char_cls, akari)

38

39 def dump(self):

40 assert self.char_cls

41 return self.char_cls.__name__ if not self.akari else 'Akari'

42

43 def sync(self, data) -> None:

44 from thb.characters.base import Character

45 self.set(Character.classes[data], False)

46

47 def conceal(self) -> None:

48 self.char_cls = None

49 self.chosen = None

50 self.akari = False

51

52 def set(self, char_cls, akari=False) -> None:

53 self.char_cls = char_cls

54 g = self.game

55

56 if akari:

57 self.akari = True

58 if g.is_client_side():

59 from thb import characters

60 self.char_cls = characters.akari.Akari

61

62 def __repr__(self):

63 return '<Choice: {}{}>'.format(

64 'None' if not self.char_cls else self.char_cls.__name__,

65 '[Akari]' if self.akari else '',

66 )

67

68

69 T = TypeVar('T', bound=Enum)

70

71

72 class PlayerRole(Generic[T], GameViralContext):

73 _role: T

74

75 def __init__(self, typ: Type[T]):

76 self._typ = typ

77 self._role = typ(0)

78

79 def __str__(self) -> str:

80 return self._role.name

81

82 def __eq__(self, other: object) -> bool:

83 if not isinstance(other, self._typ):

84 return False

85

86 return self._role == other

87

88 def dump(self) -> Any:

89 return self._role.value

90

91 def sync(self, data) -> None:

92 self._role = self._typ(data)

93

94 '''

95 def is_type(self, t: Enum) -> bool:

96 g = self.game

97 pl = g.players

98 return sync_primitive(self.identity == t, pl)

99 '''

100

101 def set(self, t: T) -> None:

102 assert isinstance(t, self._typ)

103 if self.game.is_server_side(): 103 ↛ exitline 103 didn't return from function 'set', because the condition on line 103 was never false

104 self._role = self._typ(t)

105

106 def get(self) -> T:

107 return self._role

108

109

110 def roll(g: THBattle, pl: BatchList[Player], items: Dict[Player, List[GameItem]]) -> BatchList[Player]:

111 from thb.item import European

112 roll = list(range(len(pl)))

113 g.random.shuffle(roll)

114 eu = European.get_european(g, items)

115 if eu:

116 i = pl.index(eu)

117 roll.remove(i)

118 roll.insert(0, i)

119

120 roll = sync_primitive(roll, pl)

121 roll = BatchList(pl[i] for i in roll)

122 g.emit_event('game_roll', roll)

123 return roll

124

125

126 class BuildChoicesSpec(TypedDict):

127 num: int

128 akaris: int

129

130

131 class VirtualPlayer(Player):

132

133 def __init__(self, players: Sequence[Player]):

134 self.players = players

135 super().__init__()

136

137 def reveal(self, obj: Any):

138 for p in self.players:

139 p.reveal(obj)

140

141

142 def build_choices_shared(g: THBattle,

143 players: BatchList[Player],

144 items: Dict[Player, List[GameItem]],

145 candidates: List[Type[Character]],

146 spec: BuildChoicesSpec,

147 ) -> Tuple[List[CharChoice], Dict[Player, CharChoice]]:

148 p = VirtualPlayer(players)

149 choices, imperial = build_choices(g, players, items, candidates, {p: spec})

150 return choices[p], imperial

151

152

153 def build_choices(g: THBattle,

154 players: BatchList[Player],

155 items: Dict[Player, List[GameItem]],

156 candidates: List[Type[Character]],

157 spec: Dict[Player, BuildChoicesSpec],

158 ) -> Tuple[Dict[Player, List[CharChoice]], Dict[Player, CharChoice]]:

159

160 from thb.item import ImperialChoice

161

162 # ----- testing -----

163 from thb.characters.base import Character

164

165 testing_lst: Iterable[str] = settings.TESTING_CHARACTERS

166 testing = list(Character.classes[i] for i in testing_lst)

167 candidates, _ = partition(lambda c: c not in testing, candidates)

168

169 if g.is_server_side():

170 candidates = list(candidates)

171 g.random.shuffle(candidates)

172 else:

173 candidates = [None] * len(candidates)

174

175 assert sum(s['num'] for p, s in spec.items()) <= len(candidates) + len(testing), 'Insufficient choices'

176

177 result: Dict[Player, List[CharChoice]] = {p: [] for p in spec}

178

179 players_for_testing = list(spec)

180

181 candidates = list(candidates)

182 seed = get_seed_for(g, players)

183 shuffler = random.Random(seed)

184 shuffler.shuffle(players_for_testing)

185

186 for e, cls in zip(cycle(players_for_testing), testing):

187 result[e].append(CharChoice(cls))

188

189 # ----- imperial (force chosen by ImperialChoice) -----

190 imperial = ImperialChoice.get_chosen(items, players)

191 imperial = {p: CharChoice(cls) for p, cls in imperial.items()}

192

193 for p, c in imperial.items():

194 result[p].append(c)

195

196 # ----- normal -----

197 for p, s in spec.items():

198 for _ in range(len(result[p]), s['num']):

199 result[p].append(CharChoice(candidates.pop()))

200

201 # ----- akaris -----

202 if g.is_server_side():

203 rest = candidates

204 else:

205 rest = [None] * len(candidates)

206

207 g.random.shuffle(rest)

208

209 for p, s in spec.items():

210 for i in range(-s['akaris'], 0):

211 result[p][i].set(rest.pop(), True)

212

213 # ----- compose final result, reveal, and return -----

214 for p, l in result.items():

215 p.reveal(l)

216

217 return result, imperial