# Coverage for server/base.py : 90%

## 195 statements 178 run 17 missing 4 excluded 4 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): 103 ↛ 105 line 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 ↛ 113 line 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 ↛ 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):

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 ↛ 270 line 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

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