Coverage for server/base.py : 59%
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):
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:
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: 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}')
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): 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)
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): 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
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':
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()