# Coverage for server/base.py : 59%

## 195 statements 125 run 70 missing 4 excluded 5 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 copy import copy

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

7 import logging

8

9# -- third party --

10 from gevent import Greenlet, iwait

11 from gevent.pool import Group as GreenletGroup

12

13# -- own --

14 from endpoint import EndpointDied

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

16 from game.base import TimeLimitExceeded

17 from server.endpoint import Client

18

19# -- typing --

20 if TYPE_CHECKING:

21 from server.core import Core # noqa: F401

22

23

24# -- code --

25 log = logging.getLogger('Game_Server')

26

27

28 class 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

51 class InputWaiterGroup(GreenletGroup):

52 greenlet_class = InputWaiter

53

54

55 class 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 ↛ 159 line 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 ↛ 165 line 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 ↛ 176 line 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 ↛ 240 2 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

273 class 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

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