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): 103 ↛ 105line 103 didn't jump to line 105, because the condition on line 103 was never false

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: 112 ↛ 113line 112 didn't jump to line 113, because the condition on line 112 was never true

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): 

158 if isinstance(e, Player): 

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): 

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): 

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': 266 ↛ 270line 266 didn't jump to line 270, because the condition on line 266 was never false

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()