Coverage for server/base.py : 90%
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
4# -- stdlib --
5from copy import copy
6from typing import Any, Callable, Dict, Optional, Sequence, TYPE_CHECKING, Tuple, cast
7import logging
9# -- third party --
10from gevent import Greenlet, iwait
11from gevent.pool import Group as GreenletGroup
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
19# -- typing --
20if TYPE_CHECKING:
21 from server.core import Core # noqa: F401
24# -- code --
25log = logging.getLogger('Game_Server')
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
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
47 def __repr__(self) -> str:
48 return f'InputWaiter:[{self.tag}]'
51class InputWaiterGroup(GreenletGroup):
52 greenlet_class = InputWaiter
55class ServerGameRunner(GameRunner):
57 game: Game
58 core: Core
60 def __init__(self, core: Core, g: Game):
61 self.core = core
62 self.game = g
63 super().__init__()
65 def _run(self) -> None:
66 core = self.core
67 g = self.game
69 gid = core.room.gid_of(g)
70 self.gr_name = f'{repr(g)}:{gid}'
72 g.runner = self
73 g.synctag = 0
75 core.events.game_started.emit(g)
77 params = core.game.params_of(g)
78 players = core.game.players_of(g)
80 m: Dict[int, Player] = {
81 core.auth.uid_of(p.client): p
82 for p in players if isinstance(p, HumanPlayer)
83 }
85 items = {m[k]: v for k, v in core.item.items_of(g).items()}
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
97 def get_side(self) -> str:
98 return 'server'
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'
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)
115 def is_aborted(self) -> bool:
116 core = self.core
117 return core.game.is_aborted(self.game)
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)
131 core = self.core
133 assert entities
134 assert type in ('single', 'all', 'any')
135 assert not type == 'single' or len(entities) == 1
137 timeout = max(0, timeout)
139 inputlet.timeout = timeout
140 g = cast(Game, trans.game)
142 entities = list(entities)
144 t = {'single': '', 'all': '&', 'any': '|'}[type]
145 tag = 'I{0}:{1}:'.format(t, inputlet.tag())
147 ilets = {e: copy(inputlet) for e in entities}
148 for e in entities:
149 ilets[e].actor = e
151 results = {e: None for e in entities}
152 synctags = {e: g.get_synctag() for e in entities}
154 orig_entities = entities[:]
155 waiters = InputWaiterGroup()
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}')
167 e2p = {e: get_player(e) for e in entities}
168 p2e = {p: e for e, p in e2p.items()}
170 try:
171 inputany_entity = None
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)
185 for e in entities:
186 g.emit_event('user_input_start', (trans, ilets[e]))
188 bottom_halves: Any = [] # FIXME: proper typing
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)
195 g.emit_event('user_input_finish', (trans, my, rst))
197 bottom_halves[:] = []
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
209 e = p2e[p]
210 my = ilets[e]
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
220 rst = my.post_process(e, rst)
222 bottom_halves.append((
223 'R{}{}'.format(tag, synctags[e]), data, trans, my, rst
224 ))
226 entities.remove(e)
227 results[e] = rst
229 if type != 'any':
230 flush()
232 if type == 'any' and rst is not None:
233 inputany_entity = e
234 break
236 except TimeLimitExceeded:
237 pass
239 finally:
240 waiters.kill()
242 # flush bottom halves
243 flush()
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)
257 if type == 'single':
258 return results[orig_entities[0]]
260 elif type == 'any':
261 if not inputany_entity:
262 return None, None
264 return inputany_entity, results[inputany_entity]
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}
270 assert False, 'WTF?!'
273class HumanPlayer(Player):
274 client: Client
276 def __init__(self, g: Game, uid: int, client: Client):
277 self.game = g
278 self.uid = uid
279 self.client = client
281 def reveal(self, obj: Any) -> None:
282 g = self.game
283 core = cast(ServerGameRunner, g.runner).core
284 st = g.get_synctag()
286 if isinstance(obj, (list, tuple)):
287 encoded = [o.dump() for o in obj]
288 else:
289 encoded = obj.dump()
291 core.game.write(g, self.client, f'Sync:{st}', encoded)
294class NPCPlayer(Player):
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
302 def reveal(self, ol: Any) -> None:
303 self.game.get_synctag()