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

2from __future__ import annotations 

3 

4# -- stdlib -- 

5from copy import copy 

6from typing import Any, Callable, Dict, Optional, Sequence, TYPE_CHECKING, Tuple, cast 

7import logging 

8 

9# -- third party -- 

10from gevent import Greenlet, iwait 

11from gevent.pool import Group as GreenletGroup 

12 

13# -- own -- 

14from endpoint import EndpointDied 

15from game.base import Game, GameAbort, GameEnded, GameRunner, InputTransaction, Inputlet, Player 

16from game.base import TimeLimitExceeded 

17from server.endpoint import Client 

18 

19# -- typing -- 

20if TYPE_CHECKING: 

21 from server.core import Core # noqa: F401 

22 

23 

24# -- code -- 

25log = logging.getLogger('Game_Server') 

26 

27 

28class InputWaiter(Greenlet): 

29 def __init__(self, runner: ServerGameRunner, player: HumanPlayer, tag: str): 

30 Greenlet.__init__(self) 

31 self.runner = runner 

32 self.player = player 

33 self.tag = tag 

34 

35 def _run(self) -> Optional[Tuple[str, Any]]: 

36 p, t = self.player, self.tag 

37 core = self.runner.core 

38 g = self.runner.game 

39 try: 

40 # should be [tag, <Data for Inputlet.parse>] 

41 # tag likes 'I?:ChooseOption:2345' 

42 tag, rst = core.game.gamedata_of(g, p.client).gexpect([t]) 

43 return rst 

44 except EndpointDied: 

45 return None 

46 

47 def __repr__(self) -> str: 

48 return f'InputWaiter:[{self.tag}]' 

49 

50 

51class InputWaiterGroup(GreenletGroup): 

52 greenlet_class = InputWaiter 

53 

54 

55class ServerGameRunner(GameRunner): 

56 

57 game: Game 

58 core: Core 

59 

60 def __init__(self, core: Core, g: Game): 

61 self.core = core 

62 self.game = g 

63 super().__init__() 

64 

65 def _run(self) -> None: 

66 core = self.core 

67 g = self.game 

68 

69 gid = core.room.gid_of(g) 

70 self.gr_name = f'{repr(g)}:{gid}' 

71 

72 g.runner = self 

73 g.synctag = 0 

74 

75 core.events.game_started.emit(g) 

76 

77 params = core.game.params_of(g) 

78 players = core.game.players_of(g) 

79 

80 m: Dict[int, Player] = { 

81 core.auth.uid_of(p.client): p 

82 for p in players if isinstance(p, HumanPlayer) 

83 } 

84 

85 items = {m[k]: v for k, v in core.item.items_of(g).items()} 

86 

87 try: 

88 cls = g.bootstrap 

89 g.process_action(cls(params, items, players)) 

90 except GameEnded as e: 

91 core.game.set_winners(g, list(e.winners)) 

92 except GameAbort: 

93 # caused by last player leave, 

94 # events will be handled by lobby 

95 return 

96 

97 def get_side(self) -> str: 

98 return 'server' 

99 

100 def is_dropped(self, p: Player) -> bool: 

101 core = self.core 

102 g = self.game 

103 if isinstance(p, HumanPlayer): 

104 return not core.room.is_online(g, p.client) 

105 elif isinstance(p, NPCPlayer): 

106 return False 

107 else: 

108 assert False, f'WTF: {p} is not a Player' 

109 

110 def pause(self, time: float) -> None: 

111 core = self.core 

112 if not core.options.testing: 

113 core.runner.sleep(time) 

114 

115 def is_aborted(self) -> bool: 

116 core = self.core 

117 return core.game.is_aborted(self.game) 

118 

119 def user_input( 

120 self, 

121 entities: Sequence[Any], 

122 inputlet: Inputlet, 

123 timeout: int = 25, 

124 type: str = 'single', 

125 trans: Optional[InputTransaction] = None, 

126 ) -> Any: 

127 if not trans: 

128 with InputTransaction(inputlet.tag(), entities) as trans: 

129 return self.user_input(entities, inputlet, timeout, type, trans) 

130 

131 core = self.core 

132 

133 assert entities 

134 assert type in ('single', 'all', 'any') 

135 assert not type == 'single' or len(entities) == 1 

136 

137 timeout = max(0, timeout) 

138 

139 inputlet.timeout = timeout 

140 g = cast(Game, trans.game) 

141 

142 entities = list(entities) 

143 

144 t = {'single': '', 'all': '&', 'any': '|'}[type] 

145 tag = 'I{0}:{1}:'.format(t, inputlet.tag()) 

146 

147 ilets = {e: copy(inputlet) for e in entities} 

148 for e in entities: 

149 ilets[e].actor = e 

150 

151 results = {e: None for e in entities} 

152 synctags = {e: g.get_synctag() for e in entities} 

153 

154 orig_entities = entities[:] 

155 waiters = InputWaiterGroup() 

156 

157 def get_player(e: Any) -> Player: 

158 if isinstance(e, Player): 158 ↛ 159line 158 didn't jump to line 159, because the condition on line 158 was never true

159 return e 

160 elif hasattr(e, 'get_player'): 160 ↛ 165line 160 didn't jump to line 165, because the condition on line 160 was never false

161 p = e.get_player() 

162 assert isinstance(p, Player), f'{e}.get_player() == {p}, not a Player' 

163 return p 

164 else: 

165 raise Exception(f'Do not know how to process {e}') 

166 

167 e2p = {e: get_player(e) for e in entities} 

168 p2e = {p: e for e, p in e2p.items()} 

169 

170 try: 

171 inputany_entity = None 

172 

173 for e in entities: 

174 p = e2p[e] 

175 if isinstance(p, NPCPlayer): 175 ↛ 176line 175 didn't jump to line 176, because the condition on line 175 was never true

176 ilet = ilets[e] 

177 p.handle_user_input(trans, ilet) 

178 w = Greenlet(lambda _: ilet.data(), None) 

179 w.player = p 

180 waiters.start(w) 

181 else: 

182 t = tag + str(synctags[e]) 

183 waiters.spawn(self, p, t) 

184 

185 for e in entities: 

186 g.emit_event('user_input_start', (trans, ilets[e])) 

187 

188 bottom_halves: Any = [] # FIXME: proper typing 

189 

190 def flush() -> None: 

191 for t, data, trans, my, rst in bottom_halves: 

192 for u in core.room.users_of(g): 

193 core.game.write(g, u, t, data) 

194 

195 g.emit_event('user_input_finish', (trans, my, rst)) 

196 

197 bottom_halves[:] = [] 

198 

199 for w in iwait(waiters, timeout=timeout + 5): 199 ↛ 200,   199 ↛ 2402 missed branches: 1) line 199 didn't jump to line 200, because the loop on line 199 never started, 2) line 199 didn't jump to line 240, because the loop on line 199 didn't complete

200 try: 

201 rst = w.get() 

202 p, data = w.player, rst 

203 except Exception: 

204 if core.options.testing: 

205 raise 

206 log.exception('Error waiting user input, returning default') 

207 p, data = w.player, None 

208 

209 e = p2e[p] 

210 my = ilets[e] 

211 

212 try: 

213 rst = my.parse(data) 

214 except Exception: 

215 if core.options.testing: 

216 raise 

217 log.exception('user_input: exception in .process()') 

218 rst = None 

219 

220 rst = my.post_process(e, rst) 

221 

222 bottom_halves.append(( 

223 'R{}{}'.format(tag, synctags[e]), data, trans, my, rst 

224 )) 

225 

226 entities.remove(e) 

227 results[e] = rst 

228 

229 if type != 'any': 

230 flush() 

231 

232 if type == 'any' and rst is not None: 

233 inputany_entity = e 

234 break 

235 

236 except TimeLimitExceeded: 

237 pass 

238 

239 finally: 

240 waiters.kill() 

241 

242 # flush bottom halves 

243 flush() 

244 

245 # timed-out entities 

246 for e in entities: 

247 my = ilets[e] 

248 rst = my.parse(None) 

249 rst = my.post_process(e, rst) 

250 results[e] = rst 

251 g.emit_event('user_input_finish', (trans, my, rst)) 

252 core = self.core 

253 t = 'R{}{}'.format(tag, synctags[e]) 

254 for u in core.room.users_of(g): 

255 core.game.write(g, u, t, None) 

256 

257 if type == 'single': 

258 return results[orig_entities[0]] 

259 

260 elif type == 'any': 

261 if not inputany_entity: 

262 return None, None 

263 

264 return inputany_entity, results[inputany_entity] 

265 

266 elif type == 'all': 

267 # return OrderedDict([(p, results[p]) for p in orig_entities]) 

268 return {e: results[e] for e in orig_entities} 

269 

270 assert False, 'WTF?!' 

271 

272 

273class HumanPlayer(Player): 

274 client: Client 

275 

276 def __init__(self, g: Game, uid: int, client: Client): 

277 self.game = g 

278 self.uid = uid 

279 self.client = client 

280 

281 def reveal(self, obj: Any) -> None: 

282 g = self.game 

283 core = cast(ServerGameRunner, g.runner).core 

284 st = g.get_synctag() 

285 

286 if isinstance(obj, (list, tuple)): 

287 encoded = [o.dump() for o in obj] 

288 else: 

289 encoded = obj.dump() 

290 

291 core.game.write(g, self.client, f'Sync:{st}', encoded) 

292 

293 

294class NPCPlayer(Player): 

295 

296 def __init__(self, g: Game, name: str, handler: Callable[[InputTransaction, Inputlet], Any]): 

297 self.game = g 

298 self.uid = 0 

299 self.name = name 

300 self.handle_user_input = handler 

301 

302 def reveal(self, ol: Any) -> None: 

303 self.game.get_synctag()