Hide keyboard shortcuts

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 

3 

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 

13 

14# -- third party -- 

15from gevent import Greenlet, Timeout 

16from gevent.event import Event 

17from mypy_extensions import TypedDict 

18import gevent 

19 

20# -- own -- 

21from endpoint import EndpointDied 

22from utils.misc import BatchList, exceptions 

23from utils.viral import ViralContext 

24 

25# -- typing -- 

26if TYPE_CHECKING: 

27 from server.base import Game as ServerGame # noqa: F401 

28 from server.endpoint import Client # noqa: F401 

29 

30 

31# -- code -- 

32log = logging.getLogger('Game') 

33 

34all_gameobjects = set() 

35game_objects_hierarchy = set() 

36 

37 

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

47 

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

52 

53 return cls 

54 

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 

64 

65 return value 

66 ''' 

67 

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

77 

78 

79class GameObject(object, metaclass=GameObjectMeta): 

80 pass 

81 

82 

83class TimeLimitExceeded(Timeout, GameObject): 

84 pass 

85 

86 

87class GameException(Exception, GameObject): 

88 def __init__(self, msg=None, **kwargs): 

89 Exception.__init__(self, msg) 

90 self.__dict__.update(kwargs) 

91 

92 

93class GameError(GameException): 

94 pass 

95 

96 

97class GameAbort(GameException): 

98 pass 

99 

100 

101class InterruptActionFlow(GameException): 

102 def __init__(self, unwind_to=None): 

103 GameException.__init__(self) 

104 self.unwind_to = unwind_to 

105 

106 

107class AssociatedDataViralContext(ViralContext): 

108 VIRAL_SEARCH: List[str] = [] 

109 _: dict 

110 

111 def viral_import(self, _): 

112 self._ = defaultdict(bool) 

113 

114 

115class Player(GameObject, AssociatedDataViralContext): 

116 uid: int 

117 name: str 

118 

119 def reveal(self, obj_list: Any) -> None: 

120 raise GameError('Abstract') 

121 

122 def __repr__(self): 

123 return self.__class__.__name__ 

124 

125 

126class NPC(object): 

127 __slots__ = ('name', 'input_handler') 

128 

129 def __init__(self, name, input_handler): 

130 self.name = name 

131 self.input_handler = input_handler 

132 

133 

134class GameEnded(GameException): 

135 winners: Sequence[Player] 

136 

137 def __init__(self, winners: Sequence[Player]): 

138 GameException.__init__(self) 

139 self.winners = winners 

140 

141 

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 

149 

150 

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]] 

158 

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 

170 

171 def __init__(self) -> None: 

172 self.game = self 

173 

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 

181 

182 self._ = {} 

183 

184 self.refresh_dispatcher() 

185 

186 def __repr__(self): 

187 return self.__class__.__name__ 

188 

189 def refresh_dispatcher(self) -> None: 

190 self.dispatcher = self.dispatcher_cls(self) 

191 

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 ) 

203 

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) 

208 

209 return self.dispatcher.emit(evt_type, data) 

210 

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 

214 

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 

221 

222 if not action.can_fire(): 

223 log.debug('action invalid %s' % action.__class__.__name__) 

224 return False 

225 

226 try: 

227 action.succeeded = False 

228 except AttributeError: 

229 pass 

230 

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 

259 

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. 

264 

265 assert rst in (True, False), 'Action.apply_action must return boolean!' 

266 try: 

267 action.succeeded = rst 

268 except AttributeError: 

269 pass 

270 

271 action = self.emit_event('action_after', action) 

272 

273 rst = action.succeeded 

274 action.done = True 

275 

276 self.emit_event('action_done', action) 

277 

278 return rst 

279 

280 def pause(self, t: float) -> None: 

281 self.runner.pause(t) 

282 

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 

286 

287 self.synctag += 1 

288 return self.synctag 

289 

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 

294 

295 def is_server_side(self) -> bool: 

296 return self.runner.get_side() == 'server' 

297 

298 def is_client_side(self) -> bool: 

299 return self.runner.get_side() == 'client' 

300 

301 def can_leave(self, p: Player) -> bool: 

302 raise GameError('Abstract') 

303 

304 

305class GameRunner(Greenlet): 

306 ''' 

307 The GameRunner class, 

308 Provide interfaces to game environment 

309 ''' 

310 

311 def _run(self) -> None: 

312 raise GameError('Abstract') 

313 

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

323 

324 def is_aborted(self) -> bool: 

325 raise GameError('Abstract') 

326 

327 def is_dropped(self, p: Player) -> bool: 

328 raise GameError('Abstract') 

329 

330 def pause(self, time: float) -> None: 

331 raise GameError('Abstract') 

332 

333 def get_side(self) -> str: 

334 raise GameError('Abstract') 

335 

336 

337class ActionShootdown(BaseException, GameObject): 

338 def __bool__(self): 

339 return False 

340 

341 

342class EventHandler(GameObject): 

343 interested: List[str] 

344 execute_before: List[str] = [] 

345 execute_after: List[str] = [] 

346 

347 arbiter: Optional[Type[EventArbiter]] = None 

348 

349 def __init__(self, g: Game): 

350 self.game = g 

351 self._ = {} 

352 

353 def __repr__(self) -> str: 

354 return f'EV:{self.__class__.__name__}' 

355 

356 def handle(self, evt_type: str, data: Any): 

357 raise GameError('Override handle function to implement EventHandler logics!') 

358 

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) 

363 

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) 

369 

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 

376 

377 table[cls.__name__] = cls(g) 

378 

379 for a, lst in arbiters.items(): 

380 eh = table[a.__name__] 

381 eh.set_handlers(EventHandler.make_list(g, lst, fold_arbiter=False)) 

382 

383 allnames = frozenset(table) 

384 

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 

388 

389 for clsname, eh in table.items(): 

390 for before in eh.execute_before: 

391 table[before].execute_after.add(clsname) 

392 

393 for after in eh.execute_after: 

394 table[after].execute_before.add(clsname) 

395 

396 lst = list(table.values()) 

397 lst.sort(key=lambda v: v.__class__.__name__) # must sync between server and client 

398 

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) 

410 

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!") 

413 

414 toposorted.extend(commit) 

415 lst = deferred 

416 

417 return toposorted 

418 

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

427 

428 for a in eh.execute_after: 

429 dependencies.add((a, eh.__name__)) 

430 

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

438 

439 

440class EventArbiter(EventHandler): 

441 handlers = () 

442 

443 def set_handlers(self, handlers): 

444 self.handlers = handlers[:] 

445 

446 

447T = TypeVar('T', bound=EventHandler) 

448 

449 

450class EventDispatcher(GameObject): 

451 game: Game 

452 _event_handlers: Sequence[EventHandler] 

453 _adhoc_ehs: List[EventHandler] 

454 _ehs_cache: Dict[str, List[EventHandler]] 

455 

456 def __init__(self, g: Game): 

457 self.game = g 

458 

459 self._adhoc_ehs = [] 

460 self._ehs_cache = {} 

461 self._event_handlers = self.populate_handlers() 

462 

463 def add_adhoc(self, eh: EventHandler): 

464 self._adhoc_ehs.insert(0, eh) 

465 

466 def remove_adhoc(self, eh: EventHandler): 

467 try: 

468 self._adhoc_ehs.remove(eh) 

469 except ValueError: 

470 pass 

471 

472 def populate_handlers(self) -> Sequence[EventHandler]: 

473 raise Exception('Override this!') 

474 

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 

481 

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__ 

487 

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) 

505 

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 

511 

512 adhoc = self._adhoc_ehs 

513 ehs = self._get_relevant_eh(evt_type) 

514 

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 

520 

521 return data 

522 

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 

530 

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

533 

534 return data 

535 

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 

540 

541 return None 

542 

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 

549 

550 self._event_handlers.remove(c) 

551 for i in c.interested: 

552 self._ehs_cache.pop(i, '') 

553 

554 return None 

555 

556 def _get_relevant_eh(self, tag: str): 

557 ehs = self._ehs_cache.get(tag) 

558 if ehs is not None: 

559 return ehs 

560 

561 ehs = [ 

562 eh for eh in self._event_handlers if 

563 tag in eh.get_interested() 

564 ] 

565 self._ehs_cache[tag] = ehs 

566 

567 return ehs 

568 

569 

570class Action(GameObject, GameViralContext): 

571 cancelled = False 

572 done = False 

573 invalid = False 

574 succeeded: bool 

575 

576 def action_shootdown_exception(self) -> None: 

577 if not self.is_valid(): 

578 raise ActionShootdown(self) 

579 

580 _ = self.game.emit_event('action_shootdown', self) 

581 assert _ is self, "You can't replace action in 'action_shootdown' event!" 

582 

583 def action_shootdown(self): 

584 try: 

585 self.action_shootdown_exception() 

586 return None 

587 

588 except ActionShootdown as e: 

589 return e 

590 

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 

597 

598 def apply_action(self): 

599 raise GameError('Override apply_action to implement Action logics!') 

600 

601 def is_valid(self): 

602 ''' 

603 Return True if this action is complete and ready to fire. 

604 ''' 

605 return True 

606 

607 def __repr__(self): 

608 return self.__class__.__name__ 

609 

610 

611class SyncPrimitive(GameObject): 

612 def __init__(self, value): 

613 self.value = value 

614 

615 def sync(self, data): 

616 self.value = self.value.__class__(data) 

617 

618 def dump(self): 

619 return self.value 

620 

621 def __repr__(self): 

622 return self.value.__repr__() 

623 

624 

625T_sync = TypeVar('T_sync', bound=Union[list, int, str, bool]) 

626 

627 

628def sync_primitive(val: T_sync, to: Union[Player, BatchList[Player]]) -> T_sync: 

629 if not to: # sync to nobody 

630 return val 

631 

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 

643 

644 return rst 

645 

646 

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 

652 

653 return sync_primitive(seed, p) 

654 

655 

656def list_shuffle(g, lst, plain_to): 

657 seed = get_seed_for(g, plain_to) 

658 

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

665 

666 

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 

676 

677 @classmethod 

678 def tag(cls): 

679 clsname = cls.__name__ 

680 assert clsname.endswith('Inputlet') 

681 return clsname[:-8] 

682 

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 

690 

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 

698 

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 

708 

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 

716 

717 def __repr__(self): 

718 return f'<I:{self.tag()}>' 

719 

720 

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) 

726 

727 def __enter__(self): 

728 return self.begin() 

729 

730 def begin(self): 

731 g = self.game 

732 g.emit_event('user_input_transaction_begin', self) 

733 return self 

734 

735 def __exit__(self, *excinfo): 

736 self.end() 

737 return False 

738 

739 def end(self): 

740 g = self.game 

741 g.emit_event('user_input_transaction_end', self) 

742 

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

748 

749 def __repr__(self): 

750 return '<T:{}>'.format(self.name) 

751 

752 

753class Packet(object): 

754 __slots__ = ('tag', 'data', 'consumed') 

755 

756 def __init__(self, tag: str, data: object): 

757 self.tag = tag 

758 self.data = data 

759 self.consumed = False 

760 

761 def __repr__(self): 

762 return 'Packet<%s, %s, %s>' % (self.tag, self.data, '_X'[self.consumed]) 

763 

764 

765class GameArchive(TypedDict): 

766 send: List[Tuple[str, Any]] # tag, data 

767 recv: List[Tuple[str, Any]] # tag, data 

768 

769 

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

781 

782 self._in_gexpect = False 

783 

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 

787 

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 

794 

795 def feed_send(self, tag: str, data: object): 

796 p = Packet(tag, data) 

797 self._send.append(p) 

798 return p 

799 

800 def get_sent(self) -> List[Packet]: 

801 return self._send 

802 

803 def is_live(self) -> bool: 

804 return self._live 

805 

806 def gexpect(self, tags: Sequence[str]): 

807 assert not isinstance(tags, str) # legacy usage 

808 assert '*' not in tags[0] # legacy usage 

809 

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 

812 

813 try: 

814 assert not self._in_gexpect, 'NOT REENTRANT' 

815 self._in_gexpect = True 

816 

817 log.debug('GAME_EXPECT: %s', repr(tags)) 

818 tags = set(tags) 

819 

820 recv = self._pending_recv 

821 e = self._has_data 

822 e.clear() 

823 

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 

831 

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 

835 

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) 

853 

854 if dropped: 854 ↛ 855line 854 didn't jump to line 855, because the condition on line 854 was never true

855 continue 

856 

857 e.wait(timeout=5) 

858 if self._dead: 

859 raise EndpointDied 

860 e.clear() 

861 finally: 

862 self._in_gexpect = False 

863 

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

871 

872 def revive(self) -> None: 

873 self._dead = False 

874 

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 } 

880 

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

886 

887 

888class GameItem(object): 

889 inventory: Dict[str, Type[GameItem]] = {} 

890 

891 # --- class --- 

892 key: str = '' 

893 args: List[type] = [] 

894 usable = False 

895 

896 title = 'ITEM-TITLE' 

897 description = 'ITEM-DESC' 

898 

899 # --- instance --- 

900 sku: str 

901 

902 # --- poison --- 

903 init: None 

904 

905 def __init__(self, *args): 

906 raise Exception('Abstract') 

907 

908 def should_usable(self, g: ServerGame, u: Client) -> None: 

909 ... 

910 

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 

916 

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 = [] 

925 

926 if key not in cls.inventory: 

927 raise exceptions.InvalidItemSKU 

928 

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 

932 

933 try: 

934 args = [T(v) for T, v in zip(cls.args, args)] 

935 except Exception: 

936 raise exceptions.InvalidItemSKU 

937 

938 o = cls(*args) 

939 o.sku = sku 

940 return o 

941 

942 

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!')