Coverage for game/base.py : 73%
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 collections import defaultdict
6from random import Random
7from typing import Any, ClassVar, Dict, List, Optional, Sequence, Set, TYPE_CHECKING, Tuple, Type, Generic
8from typing import TypeVar, Union
9import inspect
10import logging
11import random
12import types
14# -- third party --
15from gevent import Greenlet, Timeout
16from gevent.event import Event
17from mypy_extensions import TypedDict
18import gevent
20# -- own --
21from endpoint import EndpointDied
22from utils.misc import BatchList, exceptions
23from utils.viral import ViralContext
25# -- typing --
26if TYPE_CHECKING:
27 from server.base import Game as ServerGame # noqa: F401
28 from server.core import Core as ServerCore # noqa: F401
29 from server.endpoint import Client # noqa: F401
32# -- code --
33log = logging.getLogger('Game')
35all_gameobjects = set()
36game_objects_hierarchy = set()
39class GameObjectMeta(type):
40 def __new__(mcls, clsname, bases, kw):
41 from utils.codeobj import adjust
42 for k, v in kw.items():
43 if isinstance(v, (list, set)):
44 kw[k] = tuple(v) # mutable obj not allowed
45 elif isinstance(v, types.FunctionType):
46 v.__name__ = f'{clsname}.{v.__name__}'
47 v.__code__ = adjust(v.__code__, name=v.__name__)
49 cls = super().__new__(mcls, clsname, bases, kw)
50 all_gameobjects.add(cls)
51 for b in bases:
52 game_objects_hierarchy.add((b, cls))
54 return cls
56 '''
57 def __getattribute__(cls, name):
58 value = type.__getattribute__(cls, name)
59 if isinstance(value, classmethod):
60 try:
61 rep_class = cls.rep_class(cls)
62 return lambda *a, **k: value.__get__(None, rep_class)
63 except Exception:
64 pass
66 return value
67 '''
69 @staticmethod
70 def _dump_gameobject_hierarchy():
71 with open('/dev/shm/gomap.dot', 'w') as f:
72 f.write('digraph {\nrankdir=LR;\n')
73 f.write('\n'.join([
74 '"%s" -> "%s";' % (a.__name__, b.__name__)
75 for a, b in game_objects_hierarchy
76 ]))
77 f.write('}')
80class GameObject(object, metaclass=GameObjectMeta):
81 _: dict
84class TimeLimitExceeded(Timeout, GameObject):
85 pass
88class GameException(Exception, GameObject):
89 def __init__(self, msg=None, **kwargs):
90 Exception.__init__(self, msg)
91 self.__dict__.update(kwargs)
94class GameError(GameException):
95 pass
98class GameAbort(GameException):
99 pass
102class InterruptActionFlow(GameException):
103 def __init__(self, unwind_to=None):
104 GameException.__init__(self)
105 self.unwind_to = unwind_to
108class AssociatedDataViralContext(ViralContext):
109 VIRAL_SEARCH: List[str] = []
110 _: dict
112 def viral_import(self, _):
113 self._ = defaultdict(bool)
116class Player(GameObject, AssociatedDataViralContext):
117 uid: int
118 name: str
120 def reveal(self, obj_list: Any) -> None:
121 raise GameError('Abstract')
123 def __repr__(self):
124 return self.__class__.__name__
127class Nobody(Player):
129 def reveal(self, obj_list: Any) -> None:
130 pass
133class NPC(object):
134 __slots__ = ('name', 'input_handler')
136 def __init__(self, name, input_handler):
137 self.name = name
138 self.input_handler = input_handler
141class GameEnded(GameException):
142 winners: Sequence[Player]
144 def __init__(self, winners: Sequence[Player]):
145 GameException.__init__(self)
146 self.winners = winners
149class GameViralContext(object):
150 game: Game
152 def __new__(cls, *a, **k):
153 self = object.__new__(cls)
154 gr = gevent.getcurrent()
155 self.game = gr.game
156 self._ = defaultdict(bool)
157 return self
160A = TypeVar('A', bound='Action')
161EH = TypeVar('EH', bound='EventHandler')
164class Game(GameObject, Generic[A, EH]):
165 # ----- Class Variables -----
166 n_persons: ClassVar[int]
167 npc_players: ClassVar[List[NPC]] = []
168 params_def: ClassVar[Dict[str, Any]] = {}
169 bootstrap: ClassVar[Type[BootstrapAction]]
170 dispatcher_cls: ClassVar[Type[EventDispatcher]]
172 # ----- Instance Variables -----
173 game: Game
174 runner: GameRunner
175 dispatcher: EventDispatcher
176 event_observer: Optional[EventHandler]
177 action_stack: List[A]
178 hybrid_stack: List[Union[A, EH]]
179 ended: bool
180 winners: Sequence[Player]
181 random: Random
182 _: dict
184 def __init__(self) -> None:
185 self.game = self
187 self.action_stack = []
188 self.hybrid_stack = []
189 self.ended = False
190 self.winners = []
191 self.turn_count = 0
192 self.event_observer = None
193 self.synctag = 0
195 self._ = {}
197 self.refresh_dispatcher()
199 def __repr__(self):
200 return self.__class__.__name__
202 def refresh_dispatcher(self) -> None:
203 self.dispatcher = self.dispatcher_cls(self)
205 def user_input(
206 self,
207 entities: Sequence[Any],
208 inputlet: Inputlet,
209 timeout: int = 25,
210 type: str = 'single',
211 trans: Optional[InputTransaction] = None,
212 ):
213 return self.runner.user_input(
214 entities, inputlet, timeout, type, trans
215 )
217 def emit_event(self, evt_type: str, data: Any) -> Any:
218 ob = self.event_observer
219 if ob:
220 data = ob.handle(evt_type, data)
222 return self.dispatcher.emit(evt_type, data)
224 def process_action(self, action: Action) -> bool:
225 if self.ended: 225 ↛ 226line 225 didn't jump to line 226, because the condition on line 225 was never true
226 return False
228 if action.done: 228 ↛ 229line 228 didn't jump to line 229, because the condition on line 228 was never true
229 log.debug('action already done %s' % action.__class__.__name__)
230 return action.succeeded
231 elif action.cancelled or action.invalid: 231 ↛ 232line 231 didn't jump to line 232, because the condition on line 231 was never true
232 log.debug('action cancelled/invalid %s' % action.__class__.__name__)
233 return False
235 if not action.can_fire(): 235 ↛ 236line 235 didn't jump to line 236, because the condition on line 235 was never true
236 log.debug('action invalid %s' % action.__class__.__name__)
237 return False
239 try:
240 action.succeeded = False
241 except AttributeError:
242 pass
244 action = self.emit_event('action_before', action)
245 if action.done: 245 ↛ 246line 245 didn't jump to line 246, because the condition on line 245 was never true
246 log.debug('action already done %s' % action.__class__.__name__)
247 rst = action.succeeded
248 elif action.cancelled: 248 ↛ 249line 248 didn't jump to line 249, because the condition on line 248 was never true
249 log.debug('action cancelled, not firing: %s' % action.__class__.__name__)
250 rst = False
251 elif not action.can_fire(): 251 ↛ 252line 251 didn't jump to line 252, because the condition on line 251 was never true
252 log.debug('action invalid, not firing: %s' % action.__class__.__name__)
253 action.invalid = True
254 rst = False
255 else:
256 log.debug('applying action %s' % action.__class__.__name__)
257 action = self.emit_event('action_apply', action)
258 assert not action.cancelled
259 try:
260 self.action_stack.append(action)
261 self.hybrid_stack.append(action)
262 hybrid = self.hybrid_stack # noqa, when crashes pytest will show hybrid stack here by inspecting local variables
263 rst = action.apply_action()
264 except InterruptActionFlow as e:
265 if e.unwind_to is action:
266 rst = False
267 else:
268 raise
269 finally:
270 _a = self.action_stack.pop()
271 _b = self.hybrid_stack.pop()
272 assert _a is _b is action
274 # If exception occurs here,
275 # the action should be abandoned,
276 # code below makes no sense,
277 # so it's ok to ignore them.
279 assert rst in (True, False), 'Action.apply_action must return boolean!'
280 try:
281 action.succeeded = rst
282 except AttributeError:
283 pass
285 action = self.emit_event('action_after', action)
287 rst = action.succeeded
288 action.done = True
290 self.emit_event('action_done', action)
292 return rst
294 def pause(self, t: float) -> None:
295 self.runner.pause(t)
297 def get_synctag(self) -> int:
298 if self.runner.is_aborted(): 298 ↛ 299line 298 didn't jump to line 299, because the condition on line 298 was never true
299 raise GameAbort
301 self.synctag += 1
302 return self.synctag
304 def is_dropped(self, p: Player) -> bool:
305 v = self.runner.is_dropped(p)
306 log.debug('Game.is_dropped(%s) == %s', p, v)
307 return v
309 def is_server_side(self) -> bool:
310 return self.runner.get_side() == 'server'
312 def is_client_side(self) -> bool:
313 return self.runner.get_side() == 'client'
315 def can_leave(self, p: Player) -> bool:
316 raise GameError('Abstract')
319class GameRunner(Greenlet):
320 '''
321 The GameRunner class,
322 Provide interfaces to game environment
323 '''
325 def _run(self) -> None:
326 raise GameError('Abstract')
328 def user_input(
329 self,
330 entities: Sequence[Any],
331 inputlet: Inputlet,
332 timeout: int = 25,
333 type: str = 'single',
334 trans: Optional[InputTransaction] = None,
335 ):
336 raise GameError('Abstract')
338 def is_aborted(self) -> bool:
339 raise GameError('Abstract')
341 def is_dropped(self, p: Player) -> bool:
342 raise GameError('Abstract')
344 def pause(self, time: float) -> None:
345 raise GameError('Abstract')
347 def get_side(self) -> str:
348 raise GameError('Abstract')
351class ActionShootdown(BaseException, GameObject):
352 def __bool__(self):
353 return False
356class EventHandler(GameObject):
357 interested: List[str]
358 execute_before: List[str] = []
359 execute_after: List[str] = []
361 arbiter: Optional[Type[EventArbiter]] = None
363 def __init__(self, g: Game):
364 self.game = g
365 self._ = {}
367 def __repr__(self) -> str:
368 return f'EV:{self.__class__.__name__}'
370 def handle(self, evt_type: str, data: Any):
371 raise GameError('Override handle function to implement EventHandler logics!')
373 def get_interested(self):
374 interested = self.interested
375 assert isinstance(interested, (list, tuple)), "Should specify interested events! %r" % self.__class__
376 return list(interested)
378 @staticmethod
379 def make_list(g, eh_classes, fold_arbiter=True):
380 table = {}
381 eh_classes = set(eh_classes)
382 arbiters: Any = defaultdict(list)
384 for cls in eh_classes:
385 assert not issubclass(cls, EventArbiter), 'Should not pass arbiters in make_list, %r' % cls
386 grp = cls.arbiter if fold_arbiter else None
387 if grp is not None: 387 ↛ 388line 387 didn't jump to line 388, because the condition on line 387 was never true
388 arbiters[grp].append(cls)
389 cls = grp
391 table[cls.__name__] = cls(g)
393 for a, lst in arbiters.items(): 393 ↛ 394line 393 didn't jump to line 394, because the loop on line 393 never started
394 eh = table[a.__name__]
395 eh.set_handlers(EventHandler.make_list(g, lst, fold_arbiter=False))
397 allnames = frozenset(table)
399 for eh in table.values():
400 eh.execute_before = set(eh.execute_before) & allnames # make it instance var
401 eh.execute_after = set(eh.execute_after) & allnames
403 for clsname, eh in table.items():
404 for before in eh.execute_before:
405 table[before].execute_after.add(clsname)
407 for after in eh.execute_after:
408 table[after].execute_before.add(clsname)
410 lst = list(table.values())
411 lst.sort(key=lambda v: v.__class__.__name__) # must sync between server and client
413 toposorted = []
414 while lst:
415 deferred = []
416 commit = []
417 for eh in lst:
418 if not eh.execute_after:
419 for b in eh.execute_before:
420 table[b].execute_after.remove(eh.__class__.__name__)
421 commit.append(eh)
422 else:
423 deferred.append(eh)
425 if not commit: 425 ↛ 426line 425 didn't jump to line 426, because the condition on line 425 was never true
426 raise GameError("Can't resolve dependencies! Check for circular reference!")
428 toposorted.extend(commit)
429 lst = deferred
431 return toposorted
433 @staticmethod
434 def _dump_eh_dependency_graph():
435 ehs: Set[Type[EventHandler]] = {i for i in all_gameobjects if issubclass(i, EventHandler)}
436 ehs.remove(EventHandler)
437 dependencies = set()
438 for eh in ehs:
439 for b in eh.execute_before:
440 dependencies.add((eh.__name__, b))
442 for a in eh.execute_after:
443 dependencies.add((a, eh.__name__))
445 with open('/dev/shm/eh_relations.dot', 'w') as f:
446 f.write('digraph {\nrankdir=LR;\n')
447 f.write('\n'.join([
448 '%s -> %s;' % (a, b)
449 for a, b in dependencies
450 ]))
451 f.write('}')
454class EventArbiter(EventHandler):
455 handlers: Sequence[EventHandler] = []
457 def set_handlers(self, handlers):
458 self.handlers = handlers[:]
461T = TypeVar('T', bound=EventHandler)
464class EventDispatcher(GameObject):
465 game: Game
466 _event_handlers: List[EventHandler]
467 _adhoc_ehs: List[EventHandler]
468 _ehs_cache: Dict[str, List[EventHandler]]
470 def __init__(self, g: Game):
471 self.game = g
473 self._adhoc_ehs = []
474 self._ehs_cache = {}
475 self._event_handlers = self.populate_handlers()
477 def add_adhoc(self, eh: EventHandler):
478 self._adhoc_ehs.insert(0, eh)
480 def remove_adhoc(self, eh: EventHandler):
481 try:
482 self._adhoc_ehs.remove(eh)
483 except ValueError:
484 pass
486 def populate_handlers(self) -> List[EventHandler]:
487 raise Exception('Override this!')
489 def emit(self, evt_type: str, data: Any):
490 '''
491 Fire an event, all relevant event handlers will see this,
492 data can be modified.
493 '''
494 random.random() < 0.01 and gevent.idle() # prevent buggy logic code block scheduling
496 if log.isEnabledFor(logging.DEBUG): 496 ↛ 497line 496 didn't jump to line 497, because the condition on line 496 was never true
497 if isinstance(data, (list, tuple, str)) or hasattr(data, '__dataclass_fields__'):
498 s = data
499 else:
500 s = data.__class__.__name__
502 if evt_type in ('action_before', 'action_apply', 'action_after'):
503 co_argnames = inspect.getfullargspec(data.__init__).args
504 try:
505 co_argnames.remove('self')
506 except Exception:
507 pass
508 co_args = []
509 BAD = object()
510 for n in co_argnames:
511 co_arg = getattr(data, n, BAD)
512 if co_arg is not BAD:
513 co_args.append((n, co_arg))
514 co_argsstr = ', '.join(f'{name}={repr(a)}' for name, a in co_args)
515 act_repr = f'{s}({co_argsstr})'
516 log.debug('emit_event: %s %s', evt_type, act_repr)
517 else:
518 log.debug('emit_event: %s %s', evt_type, s)
520 if evt_type in ('action_before', 'action_apply', 'action_after'):
521 action_event = True
522 assert isinstance(data, Action)
523 else:
524 action_event = False
526 adhoc = self._adhoc_ehs
527 ehs = self._get_relevant_eh(evt_type)
529 for l in adhoc, ehs:
530 for eh in l:
531 data = self.handle_single_event(eh, evt_type, data)
532 if action_event and data.cancelled: 532 ↛ 533line 532 didn't jump to line 533, because the condition on line 532 was never true
533 break
535 return data
537 def handle_single_event(self, eh: EventHandler, *a, **k):
538 try:
539 self.game.hybrid_stack.append(eh)
540 data = eh.handle(*a, **k)
541 finally:
542 rst = eh is self.game.hybrid_stack.pop()
543 assert rst
545 if data is None: 545 ↛ 546line 545 didn't jump to line 546, because the condition on line 545 was never true
546 raise Exception('EventHandler %s returned None' % eh.__class__.__name__)
548 return data
550 def find_by_cls(self, cls: Type[T]) -> Optional[T]:
551 for c in self._event_handlers:
552 if isinstance(c, cls):
553 return c
555 return None
557 def remove_by_cls(self, cls: Type[T]) -> bool:
558 c: Any # stupid mypy
560 for c in self._event_handlers: 560 ↛ 564line 560 didn't jump to line 564, because the loop on line 560 didn't complete
561 if isinstance(c, cls):
562 break
563 else:
564 return False
566 for i in c.interested:
567 self._ehs_cache.pop(i, '')
569 self._event_handlers.remove(c)
571 return True
573 def _get_relevant_eh(self, tag: str):
574 ehs = self._ehs_cache.get(tag)
575 if ehs is not None:
576 return ehs
578 ehs = [
579 eh for eh in self._event_handlers if
580 tag in eh.get_interested()
581 ]
582 self._ehs_cache[tag] = ehs
584 return ehs
587class Action(GameObject, GameViralContext):
588 cancelled = False
589 done = False
590 invalid = False
591 succeeded: bool
593 def action_shootdown_exception(self) -> None:
594 if not self.is_valid(): 594 ↛ 595line 594 didn't jump to line 595, because the condition on line 594 was never true
595 raise ActionShootdown(self)
597 _ = self.game.emit_event('action_shootdown', self)
598 assert _ is self, "You can't replace action in 'action_shootdown' event!"
600 def action_shootdown(self):
601 try:
602 self.action_shootdown_exception()
603 return None
605 except ActionShootdown as e:
606 return e
608 def can_fire(self):
609 '''
610 Return true if the action can be fired.
611 '''
612 rst = self.action_shootdown()
613 return True if rst is None else rst
615 def apply_action(self):
616 raise GameError('Override apply_action to implement Action logics!')
618 def is_valid(self):
619 '''
620 Return True if this action is complete and ready to fire.
621 '''
622 return True
624 def __repr__(self):
625 return self.__class__.__name__
628class SyncPrimitive(GameObject):
629 def __init__(self, value):
630 self.value = value
632 def sync(self, data):
633 self.value = self.value.__class__(data)
635 def dump(self):
636 return self.value
638 def __repr__(self):
639 return self.value.__repr__()
642T_sync = TypeVar('T_sync', bound=Union[list, int, str, bool])
645def sync_primitive(val: T_sync, to: Union[Player, BatchList[Player]]) -> T_sync:
646 if not to: # sync to nobody 646 ↛ 647line 646 didn't jump to line 647, because the condition on line 646 was never true
647 return val
649 rst: Any = None
650 if isinstance(val, list):
651 lst = [SyncPrimitive(i) for i in val]
652 to.reveal(lst)
653 rst = val.__class__(
654 i.value for i in lst
655 )
656 else:
657 v = SyncPrimitive(val)
658 to.reveal(v)
659 rst = v.value
661 return rst
664def get_seed_for(g: Game, p: Union[Player, BatchList[Player]]):
665 if g.is_server_side(): 665 ↛ 668line 665 didn't jump to line 668, because the condition on line 665 was never false
666 seed = g.random.getrandbits(63)
667 else:
668 seed = 0
670 return sync_primitive(seed, p)
673def list_shuffle(g, lst, plain_to):
674 seed = get_seed_for(g, plain_to)
676 if seed: # cardlist owner & server
677 shuffler = random.Random(seed)
678 shuffler.shuffle(lst)
679 else: # others
680 for i in lst:
681 i.conceal()
684class Inputlet(GameObject, GameViralContext):
685 '''
686 NOTICE: Inputlet instance variable should
687 not be used as a side channel to pass infomation
688 in game logic code.
689 '''
690 initiator: Any
691 timeout: int
692 actor: Any
694 @classmethod
695 def tag(cls):
696 clsname = cls.__name__
697 assert clsname.endswith('Inputlet')
698 return clsname[:-8]
700 def parse(self, data):
701 '''
702 Process parsed data, return result,
703 return value of this func will be the return value
704 of user_input func.
705 '''
706 return None
708 def post_process(self, actor, args):
709 '''
710 This method is called after self.parse succeeded,
711 so game logic may have chance to transform (and validate)
712 input result before input process finishes.
713 '''
714 return args
716 def with_post_process(self, f):
717 '''
718 Helper method, to make this possible
719 @ilet.with_post_process
720 def process(args):
721 ...
722 '''
723 self.post_process = f # type: ignore
724 return f
726 def data(self):
727 '''
728 Encode self, used for reconstrcting
729 inputlet state from the other end.
730 Will be fed into self.process() of the other end.
731 '''
732 return None
734 def __repr__(self):
735 return f'<I:{self.tag()}>'
738class InputTransaction(GameViralContext):
739 def __init__(self, name: str, involved: Sequence[Any], **kwargs):
740 self.name = name
741 self.involved = involved[:]
742 self.__dict__.update(kwargs)
744 def __enter__(self):
745 return self.begin()
747 def begin(self):
748 g = self.game
749 g.emit_event('user_input_transaction_begin', self)
750 return self
752 def __exit__(self, *excinfo):
753 self.end()
754 return False
756 def end(self):
757 g = self.game
758 g.emit_event('user_input_transaction_end', self)
760 def notify(self, evt_name, arg):
761 '''
762 Event For UI
763 '''
764 self.game.emit_event('user_input_transaction_feedback', (self, evt_name, arg))
766 def __repr__(self):
767 return '<T:{}>'.format(self.name)
770class Packet(object):
771 __slots__ = ('tag', 'data', 'consumed')
773 def __init__(self, tag: str, data: object):
774 self.tag = tag
775 self.data = data
776 self.consumed = False
778 def __repr__(self):
779 return 'Packet<%s, %s, %s>' % (self.tag, self.data, '_X'[self.consumed])
782class GameArchive(TypedDict):
783 send: List[Tuple[str, Any]] # tag, data
784 recv: List[Tuple[str, Any]] # tag, data
787class GameData(object):
788 def __init__(self, gid: int, live=True):
789 self.gid = gid
790 self._send: List[Packet] = []
791 self._recv: List[Packet] = []
792 self._seen: Set[str] = set()
793 self._pending_recv: List[Packet] = []
794 self._has_data = Event()
795 self._dead = False
796 self._live = live
797 self._tainted: Set[str] = set()
799 self._in_gexpect = False
801 def feed_recv(self, tag: str, data: object) -> Optional[Packet]:
802 if tag in self._seen: 802 ↛ 803line 802 didn't jump to line 803, because the condition on line 802 was never true
803 return None
805 self._seen.add(tag)
806 p = Packet(tag, data)
807 self._recv.append(p)
808 self._pending_recv.append(p)
809 self._has_data.set()
810 return p
812 def feed_send(self, tag: str, data: object):
813 p = Packet(tag, data)
814 self._send.append(p)
815 return p
817 def get_sent(self) -> List[Packet]:
818 return self._send
820 def is_live(self) -> bool:
821 return self._live
823 def gexpect(self, tags: Sequence[str]):
824 assert not isinstance(tags, str) # legacy usage
825 assert '*' not in tags[0] # legacy usage
827 if self._dead: 827 ↛ 828line 827 didn't jump to line 828, because the condition on line 827 was never true
828 raise EndpointDied
830 try:
831 assert not self._in_gexpect, 'NOT REENTRANT'
832 self._in_gexpect = True
834 log.debug('GAME_EXPECT: %s', repr(tags))
836 tags = set(tags) # type: ignore
837 assert isinstance(tags, set) # stupid mypy
839 recv = self._pending_recv
840 e = self._has_data
841 e.clear()
843 while True:
844 livepkt = None
845 dropped = False
846 for i, packet in enumerate(recv):
847 if isinstance(packet, EndpointDied): 847 ↛ 848line 847 didn't jump to line 848, because the condition on line 847 was never true
848 del recv[i]
849 raise packet
851 if packet.tag == '__game_live': 851 ↛ 852line 851 didn't jump to line 852, because the condition on line 851 was never true
852 livepkt = packet
853 continue
855 if packet.tag in tags: 855 ↛ 864line 855 didn't jump to line 864, because the condition on line 855 was never false
856 log.debug('GAME_READ: %s', repr(packet))
857 del recv[i]
858 packet.consumed = True
859 if livepkt: 859 ↛ 860line 859 didn't jump to line 860, because the condition on line 859 was never true
860 self._live = True
861 tags.discard(packet.tag)
862 self._tainted |= tags
863 return [packet.tag, packet.data]
864 elif packet.tag in self._tainted:
865 log.warning('GAME_DROP: %s, GID: %s', repr(packet), self.gid)
866 del recv[i]
867 self._tainted.discard(packet.tag)
868 dropped = True
869 break
870 else:
871 log.debug('GAME_MISS: %s, EXPECTS: %s, GID: %s', repr(packet), tags, self.gid)
873 if dropped: 873 ↛ 874line 873 didn't jump to line 874, because the condition on line 873 was never true
874 continue
876 e.wait(timeout=5)
877 if self._dead: 877 ↛ 879line 877 didn't jump to line 879, because the condition on line 877 was never false
878 raise EndpointDied
879 e.clear()
880 finally:
881 self._in_gexpect = False
883 def die(self) -> None:
884 # Explanation:
885 # When sb. exit game in input state,
886 # the others must wait until his timeout exceeded.
887 # called this to break such condition.
888 self._dead = True
889 self._has_data.set()
891 def revive(self) -> None:
892 self._dead = False
894 def archive(self) -> GameArchive:
895 return {
896 'send': [(i.tag, i.data) for i in self._send],
897 'recv': [(i.tag, i.data) for i in self._recv],
898 }
900 def feed_archive(self, data: GameArchive) -> None:
901 recv = [Packet(t, d) for t, d in data['recv']]
902 self._recv = recv
903 self._pending_recv = list(recv)
904 self._has_data.set()
907class GameItem(object):
908 inventory: Dict[str, Type[GameItem]] = {}
910 # --- class ---
911 key: ClassVar[str] = ''
912 args: ClassVar[List[type]] = []
913 usable: ClassVar[bool] = False
915 # --- instance ---
916 sku: str
917 title: str = 'ITEM-TITLE'
918 description: str = 'ITEM-DESC'
920 # --- poison ---
921 init: None
923 def __init__(self, *args):
924 raise Exception('Abstract')
926 def should_usable(self, core: ServerCore, g: ServerGame, u: Client) -> None:
927 ...
929 @classmethod
930 def register(cls, item_cls):
931 assert issubclass(item_cls, cls)
932 cls.inventory[item_cls.key] = item_cls
933 return item_cls
935 @classmethod
936 def from_sku(cls, sku: str) -> GameItem:
937 if ':' in sku:
938 key, args = sku.split(':')
939 args = args.split(',')
940 else:
941 key = sku
942 args = []
944 if key not in cls.inventory:
945 raise exceptions.InvalidItemSKU
947 cls = cls.inventory[key]
948 if len(cls.args) != len(args): 948 ↛ 949line 948 didn't jump to line 949, because the condition on line 948 was never true
949 raise exceptions.InvalidItemSKU
951 try:
952 args = [T(v) for T, v in zip(cls.args, args)]
953 except Exception:
954 raise exceptions.InvalidItemSKU
956 o = cls(*args)
957 o.sku = sku
958 return o
961class BootstrapAction(Action):
962 def __init__(self, params: Dict[str, Any],
963 items: Dict[Player, List[GameItem]],
964 players: BatchList[Player]):
965 raise Exception('Override this!')