Coverage for game/base.py : 83%
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
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.endpoint import Client # noqa: F401
31# -- code --
32log = logging.getLogger('Game')
34all_gameobjects = set()
35game_objects_hierarchy = set()
38class GameObjectMeta(type):
39 def __new__(mcls, clsname, bases, kw):
40 from utils.codeobj import adjust
41 for k, v in kw.items():
42 if isinstance(v, (list, set)):
43 kw[k] = tuple(v) # mutable obj not allowed
44 elif isinstance(v, types.FunctionType):
45 v.__name__ = f'{clsname}.{v.__name__}'
46 v.__code__ = adjust(v.__code__, name=v.__name__)
48 cls = super().__new__(mcls, clsname, bases, kw)
49 all_gameobjects.add(cls)
50 for b in bases:
51 game_objects_hierarchy.add((b, cls))
53 return cls
55 '''
56 def __getattribute__(cls, name):
57 value = type.__getattribute__(cls, name)
58 if isinstance(value, classmethod):
59 try:
60 rep_class = cls.rep_class(cls)
61 return lambda *a, **k: value.__get__(None, rep_class)
62 except Exception:
63 pass
65 return value
66 '''
68 @staticmethod
69 def _dump_gameobject_hierarchy():
70 with open('/dev/shm/gomap.dot', 'w') as f:
71 f.write('digraph {\nrankdir=LR;\n')
72 f.write('\n'.join([
73 '"%s" -> "%s";' % (a.__name__, b.__name__)
74 for a, b in game_objects_hierarchy
75 ]))
76 f.write('}')
79class GameObject(object, metaclass=GameObjectMeta):
80 pass
83class TimeLimitExceeded(Timeout, GameObject):
84 pass
87class GameException(Exception, GameObject):
88 def __init__(self, msg=None, **kwargs):
89 Exception.__init__(self, msg)
90 self.__dict__.update(kwargs)
93class GameError(GameException):
94 pass
97class GameAbort(GameException):
98 pass
101class InterruptActionFlow(GameException):
102 def __init__(self, unwind_to=None):
103 GameException.__init__(self)
104 self.unwind_to = unwind_to
107class AssociatedDataViralContext(ViralContext):
108 VIRAL_SEARCH: List[str] = []
109 _: dict
111 def viral_import(self, _):
112 self._ = defaultdict(bool)
115class Player(GameObject, AssociatedDataViralContext):
116 uid: int
117 name: str
119 def reveal(self, obj_list: Any) -> None:
120 raise GameError('Abstract')
122 def __repr__(self):
123 return self.__class__.__name__
126class NPC(object):
127 __slots__ = ('name', 'input_handler')
129 def __init__(self, name, input_handler):
130 self.name = name
131 self.input_handler = input_handler
134class GameEnded(GameException):
135 winners: Sequence[Player]
137 def __init__(self, winners: Sequence[Player]):
138 GameException.__init__(self)
139 self.winners = winners
142class GameViralContext(object):
143 def __new__(cls, *a, **k):
144 self = object.__new__(cls)
145 gr = gevent.getcurrent()
146 self.game = gr.game
147 self._ = defaultdict(bool)
148 return self
151class Game(GameObject):
152 # ----- Class Variables -----
153 n_persons: ClassVar[int]
154 npc_players: ClassVar[List[NPC]] = []
155 params_def: ClassVar[Dict[str, Any]] = {}
156 bootstrap: ClassVar[Type[BootstrapAction]]
157 dispatcher_cls: ClassVar[Type[EventDispatcher]]
159 # ----- Instance Variables -----
160 game: Game
161 runner: GameRunner
162 dispatcher: EventDispatcher
163 event_observer: Optional[EventHandler]
164 action_stack: List[Action]
165 hybrid_stack: List[Union[Action, EventHandler]]
166 ended: bool
167 winners: Sequence[Player]
168 random: Random
169 _: dict
171 def __init__(self) -> None:
172 self.game = self
174 self.action_stack = []
175 self.hybrid_stack = []
176 self.ended = False
177 self.winners = []
178 self.turn_count = 0
179 self.event_observer = None
180 self.synctag = 0
182 self._ = {}
184 self.refresh_dispatcher()
186 def __repr__(self):
187 return self.__class__.__name__
189 def refresh_dispatcher(self) -> None:
190 self.dispatcher = self.dispatcher_cls(self)
192 def user_input(
193 self,
194 entities: Sequence[Any],
195 inputlet: Inputlet,
196 timeout: int = 25,
197 type: str = 'single',
198 trans: Optional[InputTransaction] = None,
199 ):
200 return self.runner.user_input(
201 entities, inputlet, timeout, type, trans
202 )
204 def emit_event(self, evt_type: str, data: Any) -> Any:
205 ob = self.event_observer
206 if ob:
207 data = ob.handle(evt_type, data)
209 return self.dispatcher.emit(evt_type, data)
211 def process_action(self, action: Action) -> bool:
212 if self.ended: 212 ↛ 213line 212 didn't jump to line 213, because the condition on line 212 was never true
213 return False
215 if action.done: 215 ↛ 216line 215 didn't jump to line 216, because the condition on line 215 was never true
216 log.debug('action already done %s' % action.__class__.__name__)
217 return action.succeeded
218 elif action.cancelled or action.invalid: 218 ↛ 219line 218 didn't jump to line 219, because the condition on line 218 was never true
219 log.debug('action cancelled/invalid %s' % action.__class__.__name__)
220 return False
222 if not action.can_fire():
223 log.debug('action invalid %s' % action.__class__.__name__)
224 return False
226 try:
227 action.succeeded = False
228 except AttributeError:
229 pass
231 action = self.emit_event('action_before', action)
232 if action.done: 232 ↛ 233line 232 didn't jump to line 233, because the condition on line 232 was never true
233 log.debug('action already done %s' % action.__class__.__name__)
234 rst = action.succeeded
235 elif action.cancelled:
236 log.debug('action cancelled, not firing: %s' % action.__class__.__name__)
237 rst = False
238 elif not action.can_fire(): 238 ↛ 239line 238 didn't jump to line 239, because the condition on line 238 was never true
239 log.debug('action invalid, not firing: %s' % action.__class__.__name__)
240 action.invalid = True
241 rst = False
242 else:
243 log.debug('applying action %s' % action.__class__.__name__)
244 action = self.emit_event('action_apply', action)
245 assert not action.cancelled
246 try:
247 self.action_stack.append(action)
248 self.hybrid_stack.append(action)
249 rst = action.apply_action()
250 except InterruptActionFlow as e:
251 if e.unwind_to is action:
252 rst = False
253 else:
254 raise
255 finally:
256 _a = self.action_stack.pop()
257 _b = self.hybrid_stack.pop()
258 assert _a is _b is action
260 # If exception occurs here,
261 # the action should be abandoned,
262 # code below makes no sense,
263 # so it's ok to ignore them.
265 assert rst in (True, False), 'Action.apply_action must return boolean!'
266 try:
267 action.succeeded = rst
268 except AttributeError:
269 pass
271 action = self.emit_event('action_after', action)
273 rst = action.succeeded
274 action.done = True
276 self.emit_event('action_done', action)
278 return rst
280 def pause(self, t: float) -> None:
281 self.runner.pause(t)
283 def get_synctag(self) -> int:
284 if self.runner.is_aborted(): 284 ↛ 285line 284 didn't jump to line 285, because the condition on line 284 was never true
285 raise GameAbort
287 self.synctag += 1
288 return self.synctag
290 def is_dropped(self, p: Player) -> bool:
291 v = self.runner.is_dropped(p)
292 log.debug('Game.is_dropped(%s) == %s', p, v)
293 return v
295 def is_server_side(self) -> bool:
296 return self.runner.get_side() == 'server'
298 def is_client_side(self) -> bool:
299 return self.runner.get_side() == 'client'
301 def can_leave(self, p: Player) -> bool:
302 raise GameError('Abstract')
305class GameRunner(Greenlet):
306 '''
307 The GameRunner class,
308 Provide interfaces to game environment
309 '''
311 def _run(self) -> None:
312 raise GameError('Abstract')
314 def user_input(
315 self,
316 entities: Sequence[Any],
317 inputlet: Inputlet,
318 timeout: int = 25,
319 type: str = 'single',
320 trans: Optional[InputTransaction] = None,
321 ):
322 raise GameError('Abstract')
324 def is_aborted(self) -> bool:
325 raise GameError('Abstract')
327 def is_dropped(self, p: Player) -> bool:
328 raise GameError('Abstract')
330 def pause(self, time: float) -> None:
331 raise GameError('Abstract')
333 def get_side(self) -> str:
334 raise GameError('Abstract')
337class ActionShootdown(BaseException, GameObject):
338 def __bool__(self):
339 return False
342class EventHandler(GameObject):
343 interested: List[str]
344 execute_before: List[str] = []
345 execute_after: List[str] = []
347 arbiter: Optional[Type[EventArbiter]] = None
349 def __init__(self, g: Game):
350 self.game = g
351 self._ = {}
353 def __repr__(self) -> str:
354 return f'EV:{self.__class__.__name__}'
356 def handle(self, evt_type: str, data: Any):
357 raise GameError('Override handle function to implement EventHandler logics!')
359 def get_interested(self):
360 interested = self.interested
361 assert isinstance(interested, (list, tuple)), "Should specify interested events! %r" % self.__class__
362 return list(interested)
364 @staticmethod
365 def make_list(g, eh_classes, fold_arbiter=True):
366 table = {}
367 eh_classes = set(eh_classes)
368 arbiters: Any = defaultdict(list)
370 for cls in eh_classes:
371 assert not issubclass(cls, EventArbiter), 'Should not pass arbiters in make_list, %r' % cls
372 grp = cls.arbiter if fold_arbiter else None
373 if grp is not None:
374 arbiters[grp].append(cls)
375 cls = grp
377 table[cls.__name__] = cls(g)
379 for a, lst in arbiters.items():
380 eh = table[a.__name__]
381 eh.set_handlers(EventHandler.make_list(g, lst, fold_arbiter=False))
383 allnames = frozenset(table)
385 for eh in table.values():
386 eh.execute_before = set(eh.execute_before) & allnames # make it instance var
387 eh.execute_after = set(eh.execute_after) & allnames
389 for clsname, eh in table.items():
390 for before in eh.execute_before:
391 table[before].execute_after.add(clsname)
393 for after in eh.execute_after:
394 table[after].execute_before.add(clsname)
396 lst = list(table.values())
397 lst.sort(key=lambda v: v.__class__.__name__) # must sync between server and client
399 toposorted = []
400 while lst:
401 deferred = []
402 commit = []
403 for eh in lst:
404 if not eh.execute_after:
405 for b in eh.execute_before:
406 table[b].execute_after.remove(eh.__class__.__name__)
407 commit.append(eh)
408 else:
409 deferred.append(eh)
411 if not commit: 411 ↛ 412line 411 didn't jump to line 412, because the condition on line 411 was never true
412 raise GameError("Can't resolve dependencies! Check for circular reference!")
414 toposorted.extend(commit)
415 lst = deferred
417 return toposorted
419 @staticmethod
420 def _dump_eh_dependency_graph():
421 ehs: Set[Type[EventHandler]] = {i for i in all_gameobjects if issubclass(i, EventHandler)}
422 ehs.remove(EventHandler)
423 dependencies = set()
424 for eh in ehs:
425 for b in eh.execute_before:
426 dependencies.add((eh.__name__, b))
428 for a in eh.execute_after:
429 dependencies.add((a, eh.__name__))
431 with open('/dev/shm/eh_relations.dot', 'w') as f:
432 f.write('digraph {\nrankdir=LR;\n')
433 f.write('\n'.join([
434 '%s -> %s;' % (a, b)
435 for a, b in dependencies
436 ]))
437 f.write('}')
440class EventArbiter(EventHandler):
441 handlers = ()
443 def set_handlers(self, handlers):
444 self.handlers = handlers[:]
447T = TypeVar('T', bound=EventHandler)
450class EventDispatcher(GameObject):
451 game: Game
452 _event_handlers: Sequence[EventHandler]
453 _adhoc_ehs: List[EventHandler]
454 _ehs_cache: Dict[str, List[EventHandler]]
456 def __init__(self, g: Game):
457 self.game = g
459 self._adhoc_ehs = []
460 self._ehs_cache = {}
461 self._event_handlers = self.populate_handlers()
463 def add_adhoc(self, eh: EventHandler):
464 self._adhoc_ehs.insert(0, eh)
466 def remove_adhoc(self, eh: EventHandler):
467 try:
468 self._adhoc_ehs.remove(eh)
469 except ValueError:
470 pass
472 def populate_handlers(self) -> Sequence[EventHandler]:
473 raise Exception('Override this!')
475 def emit(self, evt_type: str, data: Any):
476 '''
477 Fire an event, all relevant event handlers will see this,
478 data can be modified.
479 '''
480 random.random() < 0.01 and gevent.idle() # prevent buggy logic code block scheduling
482 if log.isEnabledFor(logging.DEBUG):
483 if isinstance(data, (list, tuple, str)) or hasattr(data, '__dataclass_fields__'):
484 s = data
485 else:
486 s = data.__class__.__name__
488 if evt_type in ('action_before', 'action_apply', 'action_after'):
489 co_argnames = inspect.getfullargspec(data.__init__).args
490 try:
491 co_argnames.remove('self')
492 except Exception:
493 pass
494 co_args = []
495 BAD = object()
496 for n in co_argnames:
497 co_arg = getattr(data, n, BAD)
498 if co_arg is not BAD:
499 co_args.append((n, co_arg))
500 co_argsstr = ', '.join(f'{name}={repr(a)}' for name, a in co_args)
501 act_repr = f'{s}({co_argsstr})'
502 log.debug('emit_event: %s %s', evt_type, act_repr)
503 else:
504 log.debug('emit_event: %s %s', evt_type, s)
506 if evt_type in ('action_before', 'action_apply', 'action_after'):
507 action_event = True
508 assert isinstance(data, Action)
509 else:
510 action_event = False
512 adhoc = self._adhoc_ehs
513 ehs = self._get_relevant_eh(evt_type)
515 for l in adhoc, ehs:
516 for eh in l:
517 data = self.handle_single_event(eh, evt_type, data)
518 if action_event and data.cancelled:
519 break
521 return data
523 def handle_single_event(self, eh: EventHandler, *a, **k):
524 try:
525 self.game.hybrid_stack.append(eh)
526 data = eh.handle(*a, **k)
527 finally:
528 rst = eh is self.game.hybrid_stack.pop()
529 assert rst
531 if data is None: 531 ↛ 532line 531 didn't jump to line 532, because the condition on line 531 was never true
532 raise Exception('EventHandler %s returned None' % eh.__class__.__name__)
534 return data
536 def find_by_cls(self, cls: Type[T]) -> Optional[T]:
537 for c in self._event_handlers:
538 if isinstance(c, cls):
539 return c
541 return None
543 def remove_by_cls(self, cls: Type[T]) -> bool:
544 for c in self._event_handlers: 544 ↛ 548line 544 didn't jump to line 548, because the loop on line 544 didn't complete
545 if isinstance(c, cls):
546 break
547 else:
548 return False
550 self._event_handlers.remove(c)
551 for i in c.interested:
552 self._ehs_cache.pop(i, '')
554 return None
556 def _get_relevant_eh(self, tag: str):
557 ehs = self._ehs_cache.get(tag)
558 if ehs is not None:
559 return ehs
561 ehs = [
562 eh for eh in self._event_handlers if
563 tag in eh.get_interested()
564 ]
565 self._ehs_cache[tag] = ehs
567 return ehs
570class Action(GameObject, GameViralContext):
571 cancelled = False
572 done = False
573 invalid = False
574 succeeded: bool
576 def action_shootdown_exception(self) -> None:
577 if not self.is_valid():
578 raise ActionShootdown(self)
580 _ = self.game.emit_event('action_shootdown', self)
581 assert _ is self, "You can't replace action in 'action_shootdown' event!"
583 def action_shootdown(self):
584 try:
585 self.action_shootdown_exception()
586 return None
588 except ActionShootdown as e:
589 return e
591 def can_fire(self):
592 '''
593 Return true if the action can be fired.
594 '''
595 rst = self.action_shootdown()
596 return True if rst is None else rst
598 def apply_action(self):
599 raise GameError('Override apply_action to implement Action logics!')
601 def is_valid(self):
602 '''
603 Return True if this action is complete and ready to fire.
604 '''
605 return True
607 def __repr__(self):
608 return self.__class__.__name__
611class SyncPrimitive(GameObject):
612 def __init__(self, value):
613 self.value = value
615 def sync(self, data):
616 self.value = self.value.__class__(data)
618 def dump(self):
619 return self.value
621 def __repr__(self):
622 return self.value.__repr__()
625T_sync = TypeVar('T_sync', bound=Union[list, int, str, bool])
628def sync_primitive(val: T_sync, to: Union[Player, BatchList[Player]]) -> T_sync:
629 if not to: # sync to nobody
630 return val
632 rst: Any = None
633 if isinstance(val, list):
634 lst = [SyncPrimitive(i) for i in val]
635 to.reveal(lst)
636 rst = val.__class__(
637 i.value for i in lst
638 )
639 else:
640 v = SyncPrimitive(val)
641 to.reveal(v)
642 rst = v.value
644 return rst
647def get_seed_for(g: Game, p: Union[Player, BatchList[Player]]):
648 if g.is_server_side():
649 seed = g.random.getrandbits(63)
650 else:
651 seed = 0
653 return sync_primitive(seed, p)
656def list_shuffle(g, lst, plain_to):
657 seed = get_seed_for(g, plain_to)
659 if seed: # cardlist owner & server
660 shuffler = random.Random(seed)
661 shuffler.shuffle(lst)
662 else: # others
663 for i in lst:
664 i.conceal()
667class Inputlet(GameObject, GameViralContext):
668 '''
669 NOTICE: Inputlet instance variable should
670 not be used as a side channel to pass infomation
671 in game logic code.
672 '''
673 initiator: Any
674 timeout: int
675 actor: object
677 @classmethod
678 def tag(cls):
679 clsname = cls.__name__
680 assert clsname.endswith('Inputlet')
681 return clsname[:-8]
683 def parse(self, data):
684 '''
685 Process parsed data, return result,
686 return value of this func will be the return value
687 of user_input func.
688 '''
689 return None
691 def post_process(self, actor, args):
692 '''
693 This method is called after self.parse succeeded,
694 so game logic may have chance to transform (and validate)
695 input result before input process finishes.
696 '''
697 return args
699 def with_post_process(self, f):
700 '''
701 Helper method, to make this possible
702 @ilet.with_post_process
703 def process(args):
704 ...
705 '''
706 self.post_process = f
707 return f
709 def data(self):
710 '''
711 Encode self, used for reconstrcting
712 inputlet state from the other end.
713 Will be fed into self.process() of the other end.
714 '''
715 return None
717 def __repr__(self):
718 return f'<I:{self.tag()}>'
721class InputTransaction(GameViralContext):
722 def __init__(self, name: str, involved: Sequence[Any], **kwargs):
723 self.name = name
724 self.involved = involved[:]
725 self.__dict__.update(kwargs)
727 def __enter__(self):
728 return self.begin()
730 def begin(self):
731 g = self.game
732 g.emit_event('user_input_transaction_begin', self)
733 return self
735 def __exit__(self, *excinfo):
736 self.end()
737 return False
739 def end(self):
740 g = self.game
741 g.emit_event('user_input_transaction_end', self)
743 def notify(self, evt_name, arg):
744 '''
745 Event For UI
746 '''
747 self.game.emit_event('user_input_transaction_feedback', (self, evt_name, arg))
749 def __repr__(self):
750 return '<T:{}>'.format(self.name)
753class Packet(object):
754 __slots__ = ('tag', 'data', 'consumed')
756 def __init__(self, tag: str, data: object):
757 self.tag = tag
758 self.data = data
759 self.consumed = False
761 def __repr__(self):
762 return 'Packet<%s, %s, %s>' % (self.tag, self.data, '_X'[self.consumed])
765class GameArchive(TypedDict):
766 send: List[Tuple[str, Any]] # tag, data
767 recv: List[Tuple[str, Any]] # tag, data
770class GameData(object):
771 def __init__(self, gid: int, live=True):
772 self.gid = gid
773 self._send: List[Packet] = []
774 self._recv: List[Packet] = []
775 self._seen: Set[str] = set()
776 self._pending_recv: List[Packet] = []
777 self._has_data = Event()
778 self._dead = False
779 self._live = live
780 self._tainted = set()
782 self._in_gexpect = False
784 def feed_recv(self, tag: str, data: object) -> Packet:
785 if tag in self._seen: 785 ↛ 786line 785 didn't jump to line 786, because the condition on line 785 was never true
786 return
788 self._seen.add(tag)
789 p = Packet(tag, data)
790 self._recv.append(p)
791 self._pending_recv.append(p)
792 self._has_data.set()
793 return p
795 def feed_send(self, tag: str, data: object):
796 p = Packet(tag, data)
797 self._send.append(p)
798 return p
800 def get_sent(self) -> List[Packet]:
801 return self._send
803 def is_live(self) -> bool:
804 return self._live
806 def gexpect(self, tags: Sequence[str]):
807 assert not isinstance(tags, str) # legacy usage
808 assert '*' not in tags[0] # legacy usage
810 if self._dead: 810 ↛ 811line 810 didn't jump to line 811, because the condition on line 810 was never true
811 raise EndpointDied
813 try:
814 assert not self._in_gexpect, 'NOT REENTRANT'
815 self._in_gexpect = True
817 log.debug('GAME_EXPECT: %s', repr(tags))
818 tags = set(tags)
820 recv = self._pending_recv
821 e = self._has_data
822 e.clear()
824 while True:
825 livepkt = None
826 dropped = False
827 for i, packet in enumerate(recv):
828 if isinstance(packet, EndpointDied): 828 ↛ 829line 828 didn't jump to line 829, because the condition on line 828 was never true
829 del recv[i]
830 raise packet
832 if packet.tag == '__game_live': 832 ↛ 833line 832 didn't jump to line 833, because the condition on line 832 was never true
833 livepkt = packet
834 continue
836 if packet.tag in tags:
837 log.debug('GAME_READ: %s', repr(packet))
838 del recv[i]
839 packet.consumed = True
840 if livepkt: 840 ↛ 841line 840 didn't jump to line 841, because the condition on line 840 was never true
841 self._live = True
842 tags.discard(packet.tag)
843 self._tainted |= tags
844 return [packet.tag, packet.data]
845 elif packet.tag in self._tainted: 845 ↛ 846line 845 didn't jump to line 846, because the condition on line 845 was never true
846 log.warning('GAME_DROP: %s, GID: %s', repr(packet), self.gid)
847 del recv[i]
848 self._tainted.discard(packet.tag)
849 dropped = True
850 break
851 else:
852 log.debug('GAME_MISS: %s, EXPECTS: %s, GID: %s', repr(packet), tags, self.gid)
854 if dropped: 854 ↛ 855line 854 didn't jump to line 855, because the condition on line 854 was never true
855 continue
857 e.wait(timeout=5)
858 if self._dead:
859 raise EndpointDied
860 e.clear()
861 finally:
862 self._in_gexpect = False
864 def die(self) -> None:
865 # Explanation:
866 # When sb. exit game in input state,
867 # the others must wait until his timeout exceeded.
868 # called this to break such condition.
869 self._dead = True
870 self._has_data.set()
872 def revive(self) -> None:
873 self._dead = False
875 def archive(self) -> GameArchive:
876 return {
877 'send': [(i.tag, i.data) for i in self._send],
878 'recv': [(i.tag, i.data) for i in self._recv],
879 }
881 def feed_archive(self, data: GameArchive) -> None:
882 recv = [Packet(t, d) for t, d in data['recv']]
883 self._recv = recv
884 self._pending_recv = list(recv)
885 self._has_data.set()
888class GameItem(object):
889 inventory: Dict[str, Type[GameItem]] = {}
891 # --- class ---
892 key: str = ''
893 args: List[type] = []
894 usable = False
896 title = 'ITEM-TITLE'
897 description = 'ITEM-DESC'
899 # --- instance ---
900 sku: str
902 # --- poison ---
903 init: None
905 def __init__(self, *args):
906 raise Exception('Abstract')
908 def should_usable(self, g: ServerGame, u: Client) -> None:
909 ...
911 @classmethod
912 def register(cls, item_cls):
913 assert issubclass(item_cls, cls)
914 cls.inventory[item_cls.key] = item_cls
915 return item_cls
917 @classmethod
918 def from_sku(cls, sku) -> GameItem:
919 if ':' in sku:
920 key, args = sku.split(':')
921 args = args.split(',')
922 else:
923 key = sku
924 args = []
926 if key not in cls.inventory:
927 raise exceptions.InvalidItemSKU
929 cls = cls.inventory[key]
930 if len(cls.args) != len(args): 930 ↛ 931line 930 didn't jump to line 931, because the condition on line 930 was never true
931 raise exceptions.InvalidItemSKU
933 try:
934 args = [T(v) for T, v in zip(cls.args, args)]
935 except Exception:
936 raise exceptions.InvalidItemSKU
938 o = cls(*args)
939 o.sku = sku
940 return o
943class BootstrapAction(Action):
944 def __init__(self, params: Dict[str, Any],
945 items: Dict[Player, List[GameItem]],
946 players: BatchList[Player]):
947 raise Exception('Override this!')